From 85d2c2cef3a6618fb899fc08cfb2a3477de9a231 Mon Sep 17 00:00:00 2001 From: vladar Date: Sun, 23 Oct 2016 05:13:55 +0700 Subject: [PATCH] Updated blog example --- examples/01-blog/Blog/Data/Comment.php | 25 ++++ examples/01-blog/Blog/Data/DataSource.php | 130 +++++++++++++++--- examples/01-blog/Blog/Data/Story.php | 2 - examples/01-blog/Blog/Type/BaseType.php | 21 +++ examples/01-blog/Blog/Type/CommentType.php | 61 ++++++++ .../Blog/Type/Enum/ContentFormatEnum.php | 19 +++ .../Blog/Type/Enum/ImageSizeEnumType.php | 9 +- .../01-blog/Blog/Type/Field/HtmlField.php | 49 +++++++ .../01-blog/Blog/Type/FieldDefinitions.php | 31 +++++ examples/01-blog/Blog/Type/ImageType.php | 22 +-- examples/01-blog/Blog/Type/MentionType.php | 31 +++++ examples/01-blog/Blog/Type/NodeType.php | 14 +- examples/01-blog/Blog/Type/QueryType.php | 65 +++++++-- .../01-blog/Blog/Type/Scalar/EmailType.php | 16 ++- .../{UrlType.php => UrlTypeDefinition.php} | 15 +- examples/01-blog/Blog/Type/StoryType.php | 88 ++++++------ examples/01-blog/Blog/Type/UserType.php | 18 ++- examples/01-blog/Blog/TypeSystem.php | 108 ++++++++++----- examples/01-blog/README.md | 100 +++++++++++--- examples/01-blog/index.php | 11 +- 20 files changed, 656 insertions(+), 179 deletions(-) create mode 100644 examples/01-blog/Blog/Data/Comment.php create mode 100644 examples/01-blog/Blog/Type/BaseType.php create mode 100644 examples/01-blog/Blog/Type/CommentType.php create mode 100644 examples/01-blog/Blog/Type/Enum/ContentFormatEnum.php create mode 100644 examples/01-blog/Blog/Type/Field/HtmlField.php create mode 100644 examples/01-blog/Blog/Type/FieldDefinitions.php create mode 100644 examples/01-blog/Blog/Type/MentionType.php rename examples/01-blog/Blog/Type/Scalar/{UrlType.php => UrlTypeDefinition.php} (89%) diff --git a/examples/01-blog/Blog/Data/Comment.php b/examples/01-blog/Blog/Data/Comment.php new file mode 100644 index 0000000..8c42ce8 --- /dev/null +++ b/examples/01-blog/Blog/Data/Comment.php @@ -0,0 +1,25 @@ +users = [ - 1 => new User([ - 'id' => 1, + '1' => new User([ + 'id' => '1', 'email' => 'john@example.com', 'firstName' => 'John', 'lastName' => 'Doe' ]), - 2 => new User([ - 'id' => 2, + '2' => new User([ + 'id' => '2', 'email' => 'jane@example.com', 'firstName' => 'Jane', 'lastName' => 'Doe' ]), - 3 => new User([ - 'id' => 3, + '3' => new User([ + 'id' => '3', 'email' => 'john@example.com', 'firstName' => 'John', 'lastName' => 'Doe' @@ -41,15 +41,56 @@ class DataSource ]; $this->stories = [ - 1 => new Story(['id' => 1, 'authorId' => 1]), - 2 => new Story(['id' => 2, 'authorId' => 1]), - 3 => new Story(['id' => 3, 'authorId' => 3]), + '1' => new Story(['id' => '1', 'authorId' => '1', 'body' => '

GraphQL is awesome!

']), + '2' => new Story(['id' => '2', 'authorId' => '1', 'body' => 'Test this']), + '3' => new Story(['id' => '3', 'authorId' => '3', 'body' => "This\n
story\n
spans\n
newlines"]), ]; $this->storyLikes = [ - 1 => [1, 2, 3], - 2 => [], - 3 => [1] + '1' => ['1', '2', '3'], + '2' => [], + '3' => ['1'] + ]; + + $this->comments = [ + // thread #1: + '100' => new Comment(['id' => '100', 'authorId' => '3', 'storyId' => '1', 'body' => 'Likes']), + '110' => new Comment(['id' =>'110', 'authorId' =>'2', 'storyId' => '1', 'body' => 'Reply #1', 'parentId' => '100']), + '111' => new Comment(['id' => '111', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-1', 'parentId' => '110']), + '112' => new Comment(['id' => '112', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #1-2', 'parentId' => '110']), + '113' => new Comment(['id' => '113', 'authorId' => '2', 'storyId' => '1', 'body' => 'Reply #1-3', 'parentId' => '110']), + '114' => new Comment(['id' => '114', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-4', 'parentId' => '110']), + '115' => new Comment(['id' => '115', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #1-5', 'parentId' => '110']), + '116' => new Comment(['id' => '116', 'authorId' => '1', 'storyId' => '1', 'body' => 'Reply #1-6', 'parentId' => '110']), + '117' => new Comment(['id' => '117', 'authorId' => '2', 'storyId' => '1', 'body' => 'Reply #1-7', 'parentId' => '110']), + '120' => new Comment(['id' => '120', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #2', 'parentId' => '100']), + '130' => new Comment(['id' => '130', 'authorId' => '3', 'storyId' => '1', 'body' => 'Reply #3', 'parentId' => '100']), + '200' => new Comment(['id' => '200', 'authorId' => '2', 'storyId' => '1', 'body' => 'Me2']), + '300' => new Comment(['id' => '300', 'authorId' => '3', 'storyId' => '1', 'body' => 'U2']), + + # thread #2: + '400' => new Comment(['id' => '400', 'authorId' => '2', 'storyId' => '2', 'body' => 'Me too']), + '500' => new Comment(['id' => '500', 'authorId' => '2', 'storyId' => '2', 'body' => 'Nice!']), + ]; + + $this->storyComments = [ + '1' => ['100', '200', '300'], + '2' => ['400', '500'] + ]; + + $this->commentReplies = [ + '100' => ['110', '120', '130'], + '110' => ['111', '112', '113', '114', '115', '116', '117'], + ]; + + $this->storyMentions = [ + '1' => [ + $this->users['2'] + ], + '2' => [ + $this->stories['1'], + $this->users['3'] + ] ]; } @@ -71,16 +112,16 @@ class DataSource return !empty($storiesFound) ? $storiesFound[count($storiesFound) - 1] : null; } - public function isLikedBy(Story $story, User $user) + public function isLikedBy($storyId, $userId) { - $subscribers = isset($this->storyLikes[$story->id]) ? $this->storyLikes[$story->id] : []; - return in_array($user->id, $subscribers); + $subscribers = isset($this->storyLikes[$storyId]) ? $this->storyLikes[$storyId] : []; + return in_array($userId, $subscribers); } - public function getUserPhoto(User $user, $size) + public function getUserPhoto($userId, $size) { return new Image([ - 'id' => $user->id, + 'id' => $userId, 'type' => Image::TYPE_USERPIC, 'size' => $size, 'width' => rand(100, 200), @@ -92,4 +133,55 @@ class DataSource { return array_pop($this->stories); } + + public function findStories($limit, $afterId = null) + { + $start = $afterId ? (int) array_search($afterId, array_keys($this->stories)) + 1 : 0; + return array_slice(array_values($this->stories), $start, $limit); + } + + public function findComments($storyId, $limit = 5, $afterId = null) + { + $storyComments = isset($this->storyComments[$storyId]) ? $this->storyComments[$storyId] : []; + + $start = isset($after) ? (int) array_search($afterId, $storyComments) + 1 : 0; + $storyComments = array_slice($storyComments, $start, $limit); + + return array_map( + function($commentId) { + return $this->comments[$commentId]; + }, + $storyComments + ); + } + + public function findReplies($commentId, $limit = 5, $afterId = null) + { + $commentReplies = isset($this->commentReplies[$commentId]) ? $this->commentReplies[$commentId] : []; + + $start = isset($after) ? (int) array_search($afterId, $commentReplies) + 1: 0; + $commentReplies = array_slice($commentReplies, $start, $limit); + + return array_map( + function($replyId) { + return $this->comments[$replyId]; + }, + $commentReplies + ); + } + + public function countComments($storyId) + { + return isset($this->storyComments[$storyId]) ? count($this->storyComments[$storyId]) : 0; + } + + public function countReplies($commentId) + { + return isset($this->commentReplies[$commentId]) ? count($this->commentReplies[$commentId]) : 0; + } + + public function findStoryMentions($storyId) + { + return isset($this->storyMentions[$storyId]) ? $this->storyMentions[$storyId] :[]; + } } diff --git a/examples/01-blog/Blog/Data/Story.php b/examples/01-blog/Blog/Data/Story.php index 04e7526..6101ad9 100644 --- a/examples/01-blog/Blog/Data/Story.php +++ b/examples/01-blog/Blog/Data/Story.php @@ -15,8 +15,6 @@ class Story public $isAnonymous = false; - public $isLiked = false; - public function __construct(array $data) { Utils::assign($this, $data); diff --git a/examples/01-blog/Blog/Type/BaseType.php b/examples/01-blog/Blog/Type/BaseType.php new file mode 100644 index 0000000..e598ed1 --- /dev/null +++ b/examples/01-blog/Blog/Type/BaseType.php @@ -0,0 +1,21 @@ +definition; + } +} diff --git a/examples/01-blog/Blog/Type/CommentType.php b/examples/01-blog/Blog/Type/CommentType.php new file mode 100644 index 0000000..f4c4420 --- /dev/null +++ b/examples/01-blog/Blog/Type/CommentType.php @@ -0,0 +1,61 @@ +definition = new ObjectType([ + 'name' => 'Comment', + 'fields' => function() use ($types) { + return [ + 'id' => $types->id(), + 'author' => $types->user(), + 'parent' => $types->comment(), + 'replies' => [ + 'type' => $types->listOf($types->comment()), + 'args' => [ + 'after' => $types->int(), + 'limit' => [ + 'type' => $types->int(), + 'defaultValue' => 5 + ] + ] + ], + 'totalReplyCount' => $types->int(), + + $types->htmlField('body') + ]; + }, + 'resolveField' => function($value, $args, $context, ResolveInfo $info) { + if (method_exists($this, $info->fieldName)) { + return $this->{$info->fieldName}($value, $args, $context, $info); + } else { + return $value->{$info->fieldName}; + } + } + ]); + } + + public function parent(Comment $comment, $args, AppContext $context) + { + return $context->dataSource->findReplies($comment->id, $args['limit'], $args['after']); + } + + public function replies(Comment $comment, $args, AppContext $context) + { + $args += ['after' => null]; + return $context->dataSource->findReplies($comment->id, $args['limit'], $args['after']); + } + + public function totalReplyCount(Comment $comment, $args, AppContext $context) + { + return $context->dataSource->countReplies($comment->id); + } +} diff --git a/examples/01-blog/Blog/Type/Enum/ContentFormatEnum.php b/examples/01-blog/Blog/Type/Enum/ContentFormatEnum.php new file mode 100644 index 0000000..cccc388 --- /dev/null +++ b/examples/01-blog/Blog/Type/Enum/ContentFormatEnum.php @@ -0,0 +1,19 @@ +definition = new EnumType([ + 'name' => 'ContentFormatEnum', + 'values' => [self::FORMAT_TEXT, self::FORMAT_HTML] + ]); + } +} diff --git a/examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php b/examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php index 03e9292..4a8569f 100644 --- a/examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php +++ b/examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php @@ -2,16 +2,17 @@ namespace GraphQL\Examples\Blog\Type\Enum; use GraphQL\Examples\Blog\Data\Image; +use GraphQL\Examples\Blog\Type\BaseType; use GraphQL\Type\Definition\EnumType; -class ImageSizeEnumType +class ImageSizeEnumType extends BaseType { /** - * @return EnumType + * ImageSizeEnumType constructor. */ - public static function getDefinition() + public function __construct() { - return new EnumType([ + $this->definition = new EnumType([ 'name' => 'ImageSizeEnum', 'values' => [ 'ICON' => Image::SIZE_ICON, diff --git a/examples/01-blog/Blog/Type/Field/HtmlField.php b/examples/01-blog/Blog/Type/Field/HtmlField.php new file mode 100644 index 0000000..59c9bd8 --- /dev/null +++ b/examples/01-blog/Blog/Type/Field/HtmlField.php @@ -0,0 +1,49 @@ + $name, + 'type' => $types->string(), + 'args' => [ + 'format' => [ + 'type' => $types->contentFormatEnum(), + 'defaultValue' => ContentFormatEnum::FORMAT_HTML + ], + 'maxLength' => $types->int() + ], + 'resolve' => function($object, $args) use ($objectKey) { + $html = $object->{$objectKey}; + $text = strip_tags($html); + + if (!empty($args['maxLength'])) { + $safeText = mb_substr($text, 0, $args['maxLength']); + } else { + $safeText = $text; + } + + switch ($args['format']) { + case ContentFormatEnum::FORMAT_HTML: + if ($safeText !== $text) { + // Text was truncated, so just show what's safe: + return nl2br($safeText); + } else { + return $html; + } + + case ContentFormatEnum::FORMAT_TEXT: + default: + return $safeText; + } + } + ]; + } +} diff --git a/examples/01-blog/Blog/Type/FieldDefinitions.php b/examples/01-blog/Blog/Type/FieldDefinitions.php new file mode 100644 index 0000000..f41b282 --- /dev/null +++ b/examples/01-blog/Blog/Type/FieldDefinitions.php @@ -0,0 +1,31 @@ +types = $types; + } + + private $htmlField; + + public function htmlField($name, $objectKey = null) + { + $objectKey = $objectKey ?: $name; + + return $this->htmlField ?: $this->htmlField = [ + 'name' => $name, + ]; + } +} diff --git a/examples/01-blog/Blog/Type/ImageType.php b/examples/01-blog/Blog/Type/ImageType.php index f5456e1..1508aa7 100644 --- a/examples/01-blog/Blog/Type/ImageType.php +++ b/examples/01-blog/Blog/Type/ImageType.php @@ -7,13 +7,11 @@ use GraphQL\Examples\Blog\TypeSystem; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ObjectType; -class ImageType +class ImageType extends BaseType { - public static function getDefinition(TypeSystem $types) + public function __construct(TypeSystem $types) { - $handler = new self(); - - return new ObjectType([ + $this->definition = new ObjectType([ 'name' => 'ImageType', 'fields' => [ 'id' => $types->id(), @@ -28,12 +26,20 @@ class ImageType 'height' => $types->int(), 'url' => [ 'type' => $types->url(), - 'resolve' => [$handler, 'resolveUrl'] + 'resolve' => [$this, 'resolveUrl'] ], - 'error' => [ + + // Just for the sake of example + 'fieldWithError' => [ 'type' => $types->string(), 'resolve' => function() { - throw new \Exception("This is error field"); + throw new \Exception("Field with exception"); + } + ], + 'nonNullFieldWithError' => [ + 'type' => $types->nonNull($types->string()), + 'resolve' => function() { + throw new \Exception("Non-null field with exception"); } ] ] diff --git a/examples/01-blog/Blog/Type/MentionType.php b/examples/01-blog/Blog/Type/MentionType.php new file mode 100644 index 0000000..66276d7 --- /dev/null +++ b/examples/01-blog/Blog/Type/MentionType.php @@ -0,0 +1,31 @@ +definition = new UnionType([ + 'name' => 'Mention', + 'types' => function() use ($types) { + return [ + $types->story(), + $types->user() + ]; + }, + 'resolveType' => function($value) use ($types) { + if ($value instanceof Story) { + return $types->story(); + } else if ($value instanceof User) { + return $types->user(); + } + } + ]); + } +} diff --git a/examples/01-blog/Blog/Type/NodeType.php b/examples/01-blog/Blog/Type/NodeType.php index 6b4c155..1ddde4d 100644 --- a/examples/01-blog/Blog/Type/NodeType.php +++ b/examples/01-blog/Blog/Type/NodeType.php @@ -3,30 +3,30 @@ namespace GraphQL\Examples\Blog\Type; use GraphQL\Examples\Blog\Data\Story; use GraphQL\Examples\Blog\Data\User; -use GraphQL\Examples\Blog\Image; +use GraphQL\Examples\Blog\Data\Image; use GraphQL\Examples\Blog\TypeSystem; use GraphQL\Type\Definition\InterfaceType; -class NodeType +class NodeType extends BaseType { /** + * NodeType constructor. * @param TypeSystem $types - * @return InterfaceType */ - public static function getDefinition(TypeSystem $types) + public function __construct(TypeSystem $types) { - return new InterfaceType([ + $this->definition = new InterfaceType([ 'name' => 'Node', 'fields' => [ 'id' => $types->id() ], 'resolveType' => function ($object) use ($types) { - return self::resolveType($object, $types); + return $this->resolveType($object, $types); } ]); } - public static function resolveType($object, TypeSystem $types) + public function resolveType($object, TypeSystem $types) { if ($object instanceof User) { return $types->user(); diff --git a/examples/01-blog/Blog/Type/QueryType.php b/examples/01-blog/Blog/Type/QueryType.php index 58d2dfe..d27481d 100644 --- a/examples/01-blog/Blog/Type/QueryType.php +++ b/examples/01-blog/Blog/Type/QueryType.php @@ -5,34 +5,53 @@ use GraphQL\Examples\Blog\AppContext; use GraphQL\Examples\Blog\TypeSystem; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; +use GraphQL\Type\Definition\Type; -class QueryType +class QueryType extends BaseType { - public static function getDefinition(TypeSystem $types) + public function __construct(TypeSystem $types) { - $handler = new self(); - - return new ObjectType([ + $this->definition = new ObjectType([ 'name' => 'Query', 'fields' => [ 'user' => [ 'type' => $types->user(), + 'description' => 'Returns user by id (in range of 1-5)', 'args' => [ - 'id' => [ + 'id' => $types->nonNull($types->id()) + ] + ], + 'viewer' => [ + 'type' => $types->user(), + 'description' => 'Represents currently logged-in user (for the sake of example - simply returns user with id == 1)' + ], + 'stories' => [ + 'type' => $types->listOf($types->story()), + 'description' => 'Returns subset of stories posted for this blog', + 'args' => [ + 'after' => [ 'type' => $types->id(), - 'defaultValue' => 1 + 'description' => 'Fetch stories listed after the story with this ID' + ], + 'limit' => [ + 'type' => $types->int(), + 'description' => 'Number of stories to be returned', + 'defaultValue' => 10 ] ] ], - 'viewer' => $types->user(), - 'lastStoryPosted' => $types->story(), - 'stories' => [ - 'type' => $types->listOf($types->story()), - 'args' => [] - ] + 'lastStoryPosted' => [ + 'type' => $types->story(), + 'description' => 'Returns last story posted for this blog' + ], + 'deprecatedField' => [ + 'type' => $types->string(), + 'deprecationReason' => 'This field is deprecated!' + ], + 'hello' => Type::string() ], - 'resolveField' => function($val, $args, $context, ResolveInfo $info) use ($handler) { - return $handler->{$info->fieldName}($val, $args, $context, $info); + 'resolveField' => function($val, $args, $context, ResolveInfo $info) { + return $this->{$info->fieldName}($val, $args, $context, $info); } ]); } @@ -47,8 +66,24 @@ class QueryType return $context->viewer; } + public function stories($val, $args, AppContext $context) + { + $args += ['after' => null]; + return $context->dataSource->findStories($args['limit'], $args['after']); + } + public function lastStoryPosted($val, $args, AppContext $context) { return $context->dataSource->findLatestStory(); } + + public function hello() + { + return 'Your graphql-php endpoint is ready! Use GraphiQL to browse API'; + } + + public function deprecatedField() + { + return 'You can request deprecated field, but it is not displayed in auto-generated documentation by default.'; + } } diff --git a/examples/01-blog/Blog/Type/Scalar/EmailType.php b/examples/01-blog/Blog/Type/Scalar/EmailType.php index 4154043..b3548dd 100644 --- a/examples/01-blog/Blog/Type/Scalar/EmailType.php +++ b/examples/01-blog/Blog/Type/Scalar/EmailType.php @@ -1,17 +1,21 @@ definition = new CustomScalarType([ + 'name' => 'Email', + 'serialize' => [$this, 'serialize'], + 'parseValue' => [$this, 'parseValue'], + 'parseLiteral' => [$this, 'parseLiteral'], + ]); } /** diff --git a/examples/01-blog/Blog/Type/Scalar/UrlType.php b/examples/01-blog/Blog/Type/Scalar/UrlTypeDefinition.php similarity index 89% rename from examples/01-blog/Blog/Type/Scalar/UrlType.php rename to examples/01-blog/Blog/Type/Scalar/UrlTypeDefinition.php index f1aa508..80fc156 100644 --- a/examples/01-blog/Blog/Type/Scalar/UrlType.php +++ b/examples/01-blog/Blog/Type/Scalar/UrlTypeDefinition.php @@ -5,18 +5,15 @@ use GraphQL\Language\AST\StringValue; use GraphQL\Type\Definition\ScalarType; use GraphQL\Utils; -class UrlType extends ScalarType +/** + * Class UrlTypeDefinition + * + * @package GraphQL\Examples\Blog\Type\Scalar + */ +class UrlTypeDefinition extends ScalarType { public $name = 'Url'; - /** - * @return UrlType - */ - public static function create() - { - return new self(); - } - /** * Serializes an internal value to include in a response. * diff --git a/examples/01-blog/Blog/Type/StoryType.php b/examples/01-blog/Blog/Type/StoryType.php index a4408b6..45ba212 100644 --- a/examples/01-blog/Blog/Type/StoryType.php +++ b/examples/01-blog/Blog/Type/StoryType.php @@ -13,92 +13,74 @@ use GraphQL\Type\Definition\ResolveInfo; * Class StoryType * @package GraphQL\Examples\Social\Type */ -class StoryType +class StoryType extends BaseType { const EDIT = 'EDIT'; const DELETE = 'DELETE'; const LIKE = 'LIKE'; const UNLIKE = 'UNLIKE'; + const REPLY = 'REPLY'; - const FORMAT_TEXT = 'TEXT'; - const FORMAT_HTML = 'HTML'; - - /** - * @param TypeSystem $types - * @return ObjectType - */ - public static function getDefinition(TypeSystem $types) + public function __construct(TypeSystem $types) { - // Type instance containing resolvers for field definitions - $handler = new self(); - - // Return definition for this type: - return new ObjectType([ + $this->definition = new ObjectType([ 'name' => 'Story', 'fields' => function() use ($types) { return [ 'id' => $types->id(), 'author' => $types->user(), - 'body' => [ - 'type' => $types->string(), + 'mentions' => $types->listOf($types->mention()), + 'totalCommentCount' => $types->int(), + 'comments' => [ + 'type' => $types->listOf($types->comment()), 'args' => [ - 'format' => new EnumType([ - 'name' => 'StoryFormatEnum', - 'values' => [self::FORMAT_TEXT, self::FORMAT_HTML] - ]), - 'maxLength' => $types->int() + 'after' => [ + 'type' => $types->id(), + 'description' => 'Load all comments listed after given comment ID' + ], + 'limit' => [ + 'type' => $types->int(), + 'defaultValue' => 5 + ] ] ], - 'isLiked' => $types->boolean(), 'affordances' => $types->listOf(new EnumType([ 'name' => 'StoryAffordancesEnum', 'values' => [ self::EDIT, self::DELETE, self::LIKE, - self::UNLIKE + self::UNLIKE, + self::REPLY ] - ])) + ])), + 'hasViewerLiked' => $types->boolean(), + + $types->htmlField('body'), ]; }, 'interfaces' => [ $types->node() ], - 'resolveField' => function($value, $args, $context, ResolveInfo $info) use ($handler) { - if (method_exists($handler, $info->fieldName)) { - return $handler->{$info->fieldName}($value, $args, $context, $info); + 'resolveField' => function($value, $args, $context, ResolveInfo $info) { + if (method_exists($this, $info->fieldName)) { + return $this->{$info->fieldName}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } - }, - 'containerType' => $handler + } ]); } - /** - * @param Story $story - * @param $args - * @param AppContext $context - * @return User|null - */ public function author(Story $story, $args, AppContext $context) { - if ($story->isAnonymous) { - return null; - } return $context->dataSource->findUser($story->authorId); } - /** - * @param Story $story - * @param $args - * @param AppContext $context - * @return array - */ public function affordances(Story $story, $args, AppContext $context) { $isViewer = $context->viewer === $context->dataSource->findUser($story->authorId); - $isLiked = $context->dataSource->isLikedBy($story, $context->viewer); + $isLiked = $context->dataSource->isLikedBy($story->id, $context->viewer->id); if ($isViewer) { $affordances[] = self::EDIT; @@ -111,4 +93,20 @@ class StoryType } return $affordances; } + + public function hasViewerLiked(Story $story, $args, AppContext $context) + { + return $context->dataSource->isLikedBy($story->id, $context->viewer->id); + } + + public function totalCommentCount(Story $story, $args, AppContext $context) + { + return $context->dataSource->countComments($story->id); + } + + public function comments(Story $story, $args, AppContext $context) + { + $args += ['after' => null]; + return $context->dataSource->findComments($story->id, $args['limit'], $args['after']); + } } diff --git a/examples/01-blog/Blog/Type/UserType.php b/examples/01-blog/Blog/Type/UserType.php index 0db4fe1..bf33c8d 100644 --- a/examples/01-blog/Blog/Type/UserType.php +++ b/examples/01-blog/Blog/Type/UserType.php @@ -7,13 +7,11 @@ use GraphQL\Examples\Blog\TypeSystem; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; -class UserType +class UserType extends BaseType { - public static function getDefinition(TypeSystem $types) + public function __construct(TypeSystem $types) { - $handler = new self(); - - return new ObjectType([ + $this->definition = new ObjectType([ 'name' => 'User', 'fields' => function() use ($types) { return [ @@ -33,7 +31,7 @@ class UserType 'type' => $types->string(), ], 'lastStoryPosted' => $types->story(), - 'error' => [ + 'fieldWithError' => [ 'type' => $types->string(), 'resolve' => function() { throw new \Exception("This is error field"); @@ -44,9 +42,9 @@ class UserType 'interfaces' => [ $types->node() ], - 'resolveField' => function($value, $args, $context, ResolveInfo $info) use ($handler) { - if (method_exists($handler, $info->fieldName)) { - return $handler->{$info->fieldName}($value, $args, $context, $info); + 'resolveField' => function($value, $args, $context, ResolveInfo $info) { + if (method_exists($this, $info->fieldName)) { + return $this->{$info->fieldName}($value, $args, $context, $info); } else { return $value->{$info->fieldName}; } @@ -56,7 +54,7 @@ class UserType public function photo(User $user, $args, AppContext $context) { - return $context->dataSource->getUserPhoto($user, $args['size']); + return $context->dataSource->getUserPhoto($user->id, $args['size']); } public function lastStoryPosted(User $user, $args, AppContext $context) diff --git a/examples/01-blog/Blog/TypeSystem.php b/examples/01-blog/Blog/TypeSystem.php index 71b91ec..045414b 100644 --- a/examples/01-blog/Blog/TypeSystem.php +++ b/examples/01-blog/Blog/TypeSystem.php @@ -1,24 +1,29 @@ story ?: ($this->story = StoryType::getDefinition($this)); - } - - /** - * @return ObjectType + * @return UserType */ public function user() { - return $this->user ?: ($this->user = UserType::getDefinition($this)); + return $this->user ?: ($this->user = new UserType($this)); } /** - * @return ObjectType + * @return StoryType + */ + public function story() + { + return $this->story ?: ($this->story = new StoryType($this)); + } + + /** + * @return CommentType + */ + public function comment() + { + return $this->comment ?: ($this->comment = new CommentType($this)); + } + + /** + * @return ImageType */ public function image() { - return $this->image ?: ($this->image = ImageType::getDefinition($this)); + return $this->image ?: ($this->image = new ImageType($this)); } /** - * @return ObjectType + * @return QueryType */ public function query() { - return $this->query ?: ($this->query = QueryType::getDefinition($this)); + return $this->query ?: ($this->query = new QueryType($this)); } - // Interfaces - private $nodeDefinition; + // Interface types + private $node; /** - * @return \GraphQL\Type\Definition\InterfaceType + * @return NodeType */ public function node() { - return $this->nodeDefinition ?: ($this->nodeDefinition = NodeType::getDefinition($this)); + return $this->node ?: ($this->node = new NodeType($this)); } - // Enums - private $imageSizeEnum; + // Unions types: + private $mention; /** - * @return EnumType + * @return MentionType + */ + public function mention() + { + return $this->mention ?: ($this->mention = new MentionType($this)); + } + + + // Enum types + private $imageSizeEnum; + private $contentFormatEnum; + + /** + * @return ImageSizeEnumType */ public function imageSizeEnum() { - return $this->imageSizeEnum ?: ($this->imageSizeEnum = ImageSizeEnumType::getDefinition()); + return $this->imageSizeEnum ?: ($this->imageSizeEnum = new ImageSizeEnumType()); + } + + /** + * @return ContentFormatEnum + */ + public function contentFormatEnum() + { + return $this->contentFormatEnum ?: ($this->contentFormatEnum = new ContentFormatEnum()); } // Custom Scalar types: @@ -94,17 +129,28 @@ class TypeSystem public function email() { - return $this->emailType ?: ($this->emailType = EmailType::create()); + return $this->emailType ?: ($this->emailType = new EmailType); } /** - * @return UrlType + * @return UrlTypeDefinition */ public function url() { - return $this->urlType ?: ($this->urlType = UrlType::create()); + return $this->urlType ?: ($this->urlType = new UrlTypeDefinition); } + /** + * @param $name + * @param null $objectKey + * @return array + */ + public function htmlField($name, $objectKey = null) + { + return HtmlField::build($this, $name, $objectKey); + } + + // Let's add internal types as well for consistent experience @@ -146,7 +192,7 @@ class TypeSystem } /** - * @param Type $type + * @param Type|DefinitionContainer $type * @return ListOfType */ public function listOf($type) @@ -155,7 +201,7 @@ class TypeSystem } /** - * @param $type + * @param Type|DefinitionContainer $type * @return NonNull */ public function nonNull($type) diff --git a/examples/01-blog/README.md b/examples/01-blog/README.md index f25e8a0..73a6a81 100644 --- a/examples/01-blog/README.md +++ b/examples/01-blog/README.md @@ -1,29 +1,45 @@ ## Blog Example +Simple yet full-featured example of GraphQL API. Models simple blog with Stories and Users. -Simple but full-featured example of GraphQL API. Models simple blog with Stories and Users. - -Note that graphql-php doesn't dictate you how to structure your application or data layer. -You may choose the way of using the library as you prefer. - -Best practices in GraphQL world still emerge, so feel free to post your proposals or own -examples as PRs. - -### Running locally +### Run locally ``` php -S localhost:8080 ./index.php ``` +### Test if GraphQL is running +If you open `http://localhost:8080` in browser you should see `json` response with +following message: +``` +{ + data: { + hello: "Your GraphQL endpoint is ready! Install GraphiQL to browse API" + } +} +``` + +Note that some browsers may try to download JSON file instead of showing you the response. +In this case try to install browser plugin that adds JSON support (like JSONView or similar) + +### Debugging Mode +By default GraphQL endpoint exposed at `http://localhost:8080` runs in production mode without +additional debugging tools enabled. + +In order to enable debugging mode with additional validation, error handling and reporting - +use `http://localhost:8080?debug=1` as endpoint + ### Browsing API The most convenient way to browse GraphQL API is by using [GraphiQL](https://github.com/graphql/graphiql) -But setting it up from scratch may be inconvenient. The great and easy alternative is to use one of -existing Google Chrome extensions: +But setting it up from scratch may be inconvenient. An easy alternative is to use one of +the existing Google Chrome extensions: - [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij) - [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) -Note that these extensions may be out of date, but most of the time they Just Work(TM) +Set `http://localhost:8080?debug=1` as your GraphQL endpoint/server in one of these extensions +and try clicking "Docs" button (usually in the top-right corner) to browse auto-generated +documentation. -Set up `http://localhost:8080?debug=0` as your GraphQL endpoint/server in these extensions and -execute following query: +### Running GraphQL queries +Copy following query to GraphiQL and execute (by clicking play button on top bar) ``` { @@ -31,9 +47,21 @@ execute following query: id email } + user(id: "2") { + id + email + } + stories(after: "1") { + id + body + comments { + ...CommentView + } + } lastStoryPosted { id - isLiked + hasViewerLiked + author { id photo(size: ICON) { @@ -43,19 +71,49 @@ execute following query: size width height - # Uncomment to see error reporting for failed resolvers - # error + # Uncomment following line to see validation error: + # nonExistingField + + # Uncomment to see error reporting for fields with exceptions thrown in resolvers + # fieldWithError + # nonNullFieldWithError } lastStoryPosted { id } } + body(format: HTML, maxLength: 10) + } +} + +fragment CommentView on Comment { + id + body + totalReplyCount + replies { + id + body } } ``` -### Debugging -By default this example runs in production mode without additional debugging tools enabled. +### Run your own query +Use GraphiQL autocomplete (via CTRL+space) to easily create your own query. -In order to enable debugging mode with additional validation of type definition configs, -PHP errors handling and reporting - change your endpoint to `http://localhost:8080?debug=1` +Note: GraphQL query requires at least one field per object type (to prevent accidental overfetching). +For example following query is invalid in GraphQL: + +``` +{ + viewer +} +``` + +Try copying this query and see what happens + +### Run mutation query +TODOC + +### Dig into source code +Now when you tried GraphQL API as a consumer, see how it is implemented by browsing +source code. diff --git a/examples/01-blog/index.php b/examples/01-blog/index.php index 22a1cd0..d4416ea 100644 --- a/examples/01-blog/index.php +++ b/examples/01-blog/index.php @@ -9,6 +9,7 @@ use \GraphQL\Examples\Blog\Data\DataSource; use \GraphQL\Schema; use \GraphQL\GraphQL; use \GraphQL\Type\Definition\Config; +use \GraphQL\Error\FormattedError; // Disable default PHP error reporting - we have better one for debug mode (see bellow) ini_set('display_errors', 0); @@ -49,6 +50,12 @@ try { } $data += ['query' => null, 'variables' => null]; + if (null === $data['query']) { + $data['query'] = ' + {hello} + '; + } + // GraphQL schema to be passed to query executor: $schema = new Schema([ 'query' => $typeSystem->query() @@ -73,9 +80,9 @@ try { } catch (\Exception $error) { $httpStatus = 500; if (!empty($_GET['debug'])) { - $result['extensions']['exception'] = \GraphQL\Error\FormattedError::createFromException($error); + $result['extensions']['exception'] = FormattedError::createFromException($error); } else { - $result['errors'] = \GraphQL\Error\FormattedError::create('Unexpected Error'); + $result['errors'] = [FormattedError::create('Unexpected Error')]; } }