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')];
}
}