Updated blog example

This commit is contained in:
vladar 2016-10-23 05:13:55 +07:00
parent 2ef58a615f
commit 85d2c2cef3
20 changed files with 656 additions and 179 deletions

View File

@ -0,0 +1,25 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils;
class Comment
{
public $id;
public $authorId;
public $storyId;
public $parentId;
public $body;
public $isAnonymous;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}

View File

@ -1,5 +1,6 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils;
/**
* Class DataSource
@ -12,28 +13,27 @@ namespace GraphQL\Examples\Blog\Data;
class DataSource
{
private $users = [];
private $stories = [];
private $storyLikes = [];
private $comments = [];
public function __construct()
{
$this->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' => '<h1>GraphQL is awesome!</h1>']),
'2' => new Story(['id' => '2', 'authorId' => '1', 'body' => '<a>Test this</a>']),
'3' => new Story(['id' => '3', 'authorId' => '3', 'body' => "This\n<br>story\n<br>spans\n<br>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 <b>#1</b>', '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] :[];
}
}

View File

@ -15,8 +15,6 @@ class Story
public $isAnonymous = false;
public $isLiked = false;
public function __construct(array $data)
{
Utils::assign($this, $data);

View File

@ -0,0 +1,21 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\DefinitionContainer;
class BaseType implements DefinitionContainer
{
/**
* @var Type
*/
protected $definition;
/**
* @return Type
*/
public function getDefinition()
{
return $this->definition;
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\Comment;
use GraphQL\Examples\Blog\TypeSystem;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
class CommentType extends BaseType
{
public function __construct(TypeSystem $types)
{
$this->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);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace GraphQL\Examples\Blog\Type\Enum;
use GraphQL\Examples\Blog\Type\BaseType;
use GraphQL\Type\Definition\EnumType;
class ContentFormatEnum extends BaseType
{
const FORMAT_TEXT = 'TEXT';
const FORMAT_HTML = 'HTML';
public function __construct()
{
$this->definition = new EnumType([
'name' => 'ContentFormatEnum',
'values' => [self::FORMAT_TEXT, self::FORMAT_HTML]
]);
}
}

View File

@ -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,

View File

@ -0,0 +1,49 @@
<?php
namespace GraphQL\Examples\Blog\Type\Field;
use GraphQL\Examples\Blog\Type\Enum\ContentFormatEnum;
use GraphQL\Examples\Blog\TypeSystem;
class HtmlField
{
public static function build(TypeSystem $types, $name, $objectKey = null)
{
$objectKey = $objectKey ?: $name;
return [
'name' => $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;
}
}
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\Type\Enum\ContentFormatEnum;
use GraphQL\Examples\Blog\TypeSystem;
/**
* Class Field
* @package GraphQL\Examples\Blog\Type
*/
class FieldDefinitions
{
private $types;
public function __construct(TypeSystem $types)
{
$this->types = $types;
}
private $htmlField;
public function htmlField($name, $objectKey = null)
{
$objectKey = $objectKey ?: $name;
return $this->htmlField ?: $this->htmlField = [
'name' => $name,
];
}
}

View File

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

View File

@ -0,0 +1,31 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\TypeSystem;
use GraphQL\Type\Definition\UnionType;
class MentionType extends BaseType
{
public function __construct(TypeSystem $types)
{
$this->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();
}
}
]);
}
}

View File

@ -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();

View File

@ -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.';
}
}

View File

@ -1,17 +1,21 @@
<?php
namespace GraphQL\Examples\Blog\Type\Scalar;
use GraphQL\Examples\Blog\Type\BaseType;
use GraphQL\Language\AST\StringValue;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Utils;
class EmailType extends ScalarType
class EmailType extends BaseType
{
public $name = 'Email';
public static function create()
public function __construct()
{
return new self();
$this->definition = new CustomScalarType([
'name' => 'Email',
'serialize' => [$this, 'serialize'],
'parseValue' => [$this, 'parseValue'],
'parseLiteral' => [$this, 'parseLiteral'],
]);
}
/**

View File

@ -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.
*

View File

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

View File

@ -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)

View File

@ -1,24 +1,29 @@
<?php
namespace GraphQL\Examples\Blog;
use GraphQL\Examples\Blog\Type\CommentType;
use GraphQL\Examples\Blog\Type\Enum\ContentFormatEnum;
use GraphQL\Examples\Blog\Type\Enum\ImageSizeEnumType;
use GraphQL\Examples\Blog\Type\Field\HtmlField;
use GraphQL\Examples\Blog\Type\FieldDefinitions;
use GraphQL\Examples\Blog\Type\MentionType;
use GraphQL\Examples\Blog\Type\NodeType;
use GraphQL\Examples\Blog\Type\QueryType;
use GraphQL\Examples\Blog\Type\Scalar\EmailType;
use GraphQL\Examples\Blog\Type\StoryType;
use GraphQL\Examples\Blog\Type\Scalar\UrlType;
use GraphQL\Examples\Blog\Type\Scalar\UrlTypeDefinition;
use GraphQL\Examples\Blog\Type\UserType;
use GraphQL\Examples\Blog\Type\ImageType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\DefinitionContainer;
/**
* Class TypeSystem
*
* Acts as a registry and factory for your types.
*
* As simplistic as possible for the sake of clarity of this example.
* Your own may be more dynamic (or even code-generated).
*
@ -27,65 +32,95 @@ use GraphQL\Type\Definition\Type;
class TypeSystem
{
// Object types:
private $story;
private $user;
private $story;
private $comment;
private $image;
private $query;
/**
* @return ObjectType
*/
public function story()
{
return $this->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)

View File

@ -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.

View File

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