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 <?php
namespace GraphQL\Examples\Blog\Data; namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils;
/** /**
* Class DataSource * Class DataSource
@ -12,28 +13,27 @@ namespace GraphQL\Examples\Blog\Data;
class DataSource class DataSource
{ {
private $users = []; private $users = [];
private $stories = []; private $stories = [];
private $storyLikes = []; private $storyLikes = [];
private $comments = [];
public function __construct() public function __construct()
{ {
$this->users = [ $this->users = [
1 => new User([ '1' => new User([
'id' => 1, 'id' => '1',
'email' => 'john@example.com', 'email' => 'john@example.com',
'firstName' => 'John', 'firstName' => 'John',
'lastName' => 'Doe' 'lastName' => 'Doe'
]), ]),
2 => new User([ '2' => new User([
'id' => 2, 'id' => '2',
'email' => 'jane@example.com', 'email' => 'jane@example.com',
'firstName' => 'Jane', 'firstName' => 'Jane',
'lastName' => 'Doe' 'lastName' => 'Doe'
]), ]),
3 => new User([ '3' => new User([
'id' => 3, 'id' => '3',
'email' => 'john@example.com', 'email' => 'john@example.com',
'firstName' => 'John', 'firstName' => 'John',
'lastName' => 'Doe' 'lastName' => 'Doe'
@ -41,15 +41,56 @@ class DataSource
]; ];
$this->stories = [ $this->stories = [
1 => new Story(['id' => 1, 'authorId' => 1]), '1' => new Story(['id' => '1', 'authorId' => '1', 'body' => '<h1>GraphQL is awesome!</h1>']),
2 => new Story(['id' => 2, 'authorId' => 1]), '2' => new Story(['id' => '2', 'authorId' => '1', 'body' => '<a>Test this</a>']),
3 => new Story(['id' => 3, 'authorId' => 3]), '3' => new Story(['id' => '3', 'authorId' => '3', 'body' => "This\n<br>story\n<br>spans\n<br>newlines"]),
]; ];
$this->storyLikes = [ $this->storyLikes = [
1 => [1, 2, 3], '1' => ['1', '2', '3'],
2 => [], '2' => [],
3 => [1] '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; 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] : []; $subscribers = isset($this->storyLikes[$storyId]) ? $this->storyLikes[$storyId] : [];
return in_array($user->id, $subscribers); return in_array($userId, $subscribers);
} }
public function getUserPhoto(User $user, $size) public function getUserPhoto($userId, $size)
{ {
return new Image([ return new Image([
'id' => $user->id, 'id' => $userId,
'type' => Image::TYPE_USERPIC, 'type' => Image::TYPE_USERPIC,
'size' => $size, 'size' => $size,
'width' => rand(100, 200), 'width' => rand(100, 200),
@ -92,4 +133,55 @@ class DataSource
{ {
return array_pop($this->stories); 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 $isAnonymous = false;
public $isLiked = false;
public function __construct(array $data) public function __construct(array $data)
{ {
Utils::assign($this, $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; namespace GraphQL\Examples\Blog\Type\Enum;
use GraphQL\Examples\Blog\Data\Image; use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Examples\Blog\Type\BaseType;
use GraphQL\Type\Definition\EnumType; 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', 'name' => 'ImageSizeEnum',
'values' => [ 'values' => [
'ICON' => Image::SIZE_ICON, '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\EnumType;
use GraphQL\Type\Definition\ObjectType; 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(); $this->definition = new ObjectType([
return new ObjectType([
'name' => 'ImageType', 'name' => 'ImageType',
'fields' => [ 'fields' => [
'id' => $types->id(), 'id' => $types->id(),
@ -28,12 +26,20 @@ class ImageType
'height' => $types->int(), 'height' => $types->int(),
'url' => [ 'url' => [
'type' => $types->url(), 'type' => $types->url(),
'resolve' => [$handler, 'resolveUrl'] 'resolve' => [$this, 'resolveUrl']
], ],
'error' => [
// Just for the sake of example
'fieldWithError' => [
'type' => $types->string(), 'type' => $types->string(),
'resolve' => function() { '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\Story;
use GraphQL\Examples\Blog\Data\User; use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Image; use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Examples\Blog\TypeSystem; use GraphQL\Examples\Blog\TypeSystem;
use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\InterfaceType;
class NodeType class NodeType extends BaseType
{ {
/** /**
* NodeType constructor.
* @param TypeSystem $types * @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', 'name' => 'Node',
'fields' => [ 'fields' => [
'id' => $types->id() 'id' => $types->id()
], ],
'resolveType' => function ($object) use ($types) { '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) { if ($object instanceof User) {
return $types->user(); return $types->user();

View File

@ -5,34 +5,53 @@ use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\TypeSystem; use GraphQL\Examples\Blog\TypeSystem;
use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo; 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(); $this->definition = new ObjectType([
return new ObjectType([
'name' => 'Query', 'name' => 'Query',
'fields' => [ 'fields' => [
'user' => [ 'user' => [
'type' => $types->user(), 'type' => $types->user(),
'description' => 'Returns user by id (in range of 1-5)',
'args' => [ 'args' => [
'id' => [ 'id' => $types->nonNull($types->id())
'type' => $types->id(),
'defaultValue' => 1
]
] ]
], ],
'viewer' => $types->user(), 'viewer' => [
'lastStoryPosted' => $types->story(), 'type' => $types->user(),
'description' => 'Represents currently logged-in user (for the sake of example - simply returns user with id == 1)'
],
'stories' => [ 'stories' => [
'type' => $types->listOf($types->story()), 'type' => $types->listOf($types->story()),
'args' => [] 'description' => 'Returns subset of stories posted for this blog',
'args' => [
'after' => [
'type' => $types->id(),
'description' => 'Fetch stories listed after the story with this ID'
],
'limit' => [
'type' => $types->int(),
'description' => 'Number of stories to be returned',
'defaultValue' => 10
]
] ]
], ],
'resolveField' => function($val, $args, $context, ResolveInfo $info) use ($handler) { 'lastStoryPosted' => [
return $handler->{$info->fieldName}($val, $args, $context, $info); '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) {
return $this->{$info->fieldName}($val, $args, $context, $info);
} }
]); ]);
} }
@ -47,8 +66,24 @@ class QueryType
return $context->viewer; 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) public function lastStoryPosted($val, $args, AppContext $context)
{ {
return $context->dataSource->findLatestStory(); 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 <?php
namespace GraphQL\Examples\Blog\Type\Scalar; namespace GraphQL\Examples\Blog\Type\Scalar;
use GraphQL\Examples\Blog\Type\BaseType;
use GraphQL\Language\AST\StringValue; use GraphQL\Language\AST\StringValue;
use GraphQL\Type\Definition\ScalarType; use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Utils; use GraphQL\Utils;
class EmailType extends ScalarType class EmailType extends BaseType
{ {
public $name = 'Email'; public function __construct()
public static function create()
{ {
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\Type\Definition\ScalarType;
use GraphQL\Utils; use GraphQL\Utils;
class UrlType extends ScalarType /**
* Class UrlTypeDefinition
*
* @package GraphQL\Examples\Blog\Type\Scalar
*/
class UrlTypeDefinition extends ScalarType
{ {
public $name = 'Url'; public $name = 'Url';
/**
* @return UrlType
*/
public static function create()
{
return new self();
}
/** /**
* Serializes an internal value to include in a response. * Serializes an internal value to include in a response.
* *

View File

@ -13,92 +13,74 @@ use GraphQL\Type\Definition\ResolveInfo;
* Class StoryType * Class StoryType
* @package GraphQL\Examples\Social\Type * @package GraphQL\Examples\Social\Type
*/ */
class StoryType class StoryType extends BaseType
{ {
const EDIT = 'EDIT'; const EDIT = 'EDIT';
const DELETE = 'DELETE'; const DELETE = 'DELETE';
const LIKE = 'LIKE'; const LIKE = 'LIKE';
const UNLIKE = 'UNLIKE'; const UNLIKE = 'UNLIKE';
const REPLY = 'REPLY';
const FORMAT_TEXT = 'TEXT'; public function __construct(TypeSystem $types)
const FORMAT_HTML = 'HTML';
/**
* @param TypeSystem $types
* @return ObjectType
*/
public static function getDefinition(TypeSystem $types)
{ {
// Type instance containing resolvers for field definitions $this->definition = new ObjectType([
$handler = new self();
// Return definition for this type:
return new ObjectType([
'name' => 'Story', 'name' => 'Story',
'fields' => function() use ($types) { 'fields' => function() use ($types) {
return [ return [
'id' => $types->id(), 'id' => $types->id(),
'author' => $types->user(), 'author' => $types->user(),
'body' => [ 'mentions' => $types->listOf($types->mention()),
'type' => $types->string(), 'totalCommentCount' => $types->int(),
'comments' => [
'type' => $types->listOf($types->comment()),
'args' => [ 'args' => [
'format' => new EnumType([ 'after' => [
'name' => 'StoryFormatEnum', 'type' => $types->id(),
'values' => [self::FORMAT_TEXT, self::FORMAT_HTML] 'description' => 'Load all comments listed after given comment ID'
]), ],
'maxLength' => $types->int() 'limit' => [
'type' => $types->int(),
'defaultValue' => 5
]
] ]
], ],
'isLiked' => $types->boolean(),
'affordances' => $types->listOf(new EnumType([ 'affordances' => $types->listOf(new EnumType([
'name' => 'StoryAffordancesEnum', 'name' => 'StoryAffordancesEnum',
'values' => [ 'values' => [
self::EDIT, self::EDIT,
self::DELETE, self::DELETE,
self::LIKE, self::LIKE,
self::UNLIKE self::UNLIKE,
self::REPLY
] ]
])) ])),
'hasViewerLiked' => $types->boolean(),
$types->htmlField('body'),
]; ];
}, },
'interfaces' => [ 'interfaces' => [
$types->node() $types->node()
], ],
'resolveField' => function($value, $args, $context, ResolveInfo $info) use ($handler) { 'resolveField' => function($value, $args, $context, ResolveInfo $info) {
if (method_exists($handler, $info->fieldName)) { if (method_exists($this, $info->fieldName)) {
return $handler->{$info->fieldName}($value, $args, $context, $info); return $this->{$info->fieldName}($value, $args, $context, $info);
} else { } else {
return $value->{$info->fieldName}; 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) public function author(Story $story, $args, AppContext $context)
{ {
if ($story->isAnonymous) {
return null;
}
return $context->dataSource->findUser($story->authorId); return $context->dataSource->findUser($story->authorId);
} }
/**
* @param Story $story
* @param $args
* @param AppContext $context
* @return array
*/
public function affordances(Story $story, $args, AppContext $context) public function affordances(Story $story, $args, AppContext $context)
{ {
$isViewer = $context->viewer === $context->dataSource->findUser($story->authorId); $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) { if ($isViewer) {
$affordances[] = self::EDIT; $affordances[] = self::EDIT;
@ -111,4 +93,20 @@ class StoryType
} }
return $affordances; 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\ObjectType;
use GraphQL\Type\Definition\ResolveInfo; 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(); $this->definition = new ObjectType([
return new ObjectType([
'name' => 'User', 'name' => 'User',
'fields' => function() use ($types) { 'fields' => function() use ($types) {
return [ return [
@ -33,7 +31,7 @@ class UserType
'type' => $types->string(), 'type' => $types->string(),
], ],
'lastStoryPosted' => $types->story(), 'lastStoryPosted' => $types->story(),
'error' => [ 'fieldWithError' => [
'type' => $types->string(), 'type' => $types->string(),
'resolve' => function() { 'resolve' => function() {
throw new \Exception("This is error field"); throw new \Exception("This is error field");
@ -44,9 +42,9 @@ class UserType
'interfaces' => [ 'interfaces' => [
$types->node() $types->node()
], ],
'resolveField' => function($value, $args, $context, ResolveInfo $info) use ($handler) { 'resolveField' => function($value, $args, $context, ResolveInfo $info) {
if (method_exists($handler, $info->fieldName)) { if (method_exists($this, $info->fieldName)) {
return $handler->{$info->fieldName}($value, $args, $context, $info); return $this->{$info->fieldName}($value, $args, $context, $info);
} else { } else {
return $value->{$info->fieldName}; return $value->{$info->fieldName};
} }
@ -56,7 +54,7 @@ class UserType
public function photo(User $user, $args, AppContext $context) 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) public function lastStoryPosted(User $user, $args, AppContext $context)

View File

@ -1,24 +1,29 @@
<?php <?php
namespace GraphQL\Examples\Blog; 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\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\NodeType;
use GraphQL\Examples\Blog\Type\QueryType; use GraphQL\Examples\Blog\Type\QueryType;
use GraphQL\Examples\Blog\Type\Scalar\EmailType; use GraphQL\Examples\Blog\Type\Scalar\EmailType;
use GraphQL\Examples\Blog\Type\StoryType; 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\UserType;
use GraphQL\Examples\Blog\Type\ImageType; use GraphQL\Examples\Blog\Type\ImageType;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\Type;
use GraphQL\Type\DefinitionContainer;
/** /**
* Class TypeSystem * Class TypeSystem
* *
* Acts as a registry and factory for your types. * Acts as a registry and factory for your types.
*
* As simplistic as possible for the sake of clarity of this example. * As simplistic as possible for the sake of clarity of this example.
* Your own may be more dynamic (or even code-generated). * Your own may be more dynamic (or even code-generated).
* *
@ -27,65 +32,95 @@ use GraphQL\Type\Definition\Type;
class TypeSystem class TypeSystem
{ {
// Object types: // Object types:
private $story;
private $user; private $user;
private $story;
private $comment;
private $image; private $image;
private $query; private $query;
/** /**
* @return ObjectType * @return UserType
*/
public function story()
{
return $this->story ?: ($this->story = StoryType::getDefinition($this));
}
/**
* @return ObjectType
*/ */
public function user() 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() 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() public function query()
{ {
return $this->query ?: ($this->query = QueryType::getDefinition($this)); return $this->query ?: ($this->query = new QueryType($this));
} }
// Interfaces // Interface types
private $nodeDefinition; private $node;
/** /**
* @return \GraphQL\Type\Definition\InterfaceType * @return NodeType
*/ */
public function node() public function node()
{ {
return $this->nodeDefinition ?: ($this->nodeDefinition = NodeType::getDefinition($this)); return $this->node ?: ($this->node = new NodeType($this));
} }
// Enums // Unions types:
private $imageSizeEnum; 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() 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: // Custom Scalar types:
@ -94,17 +129,28 @@ class TypeSystem
public function email() public function email()
{ {
return $this->emailType ?: ($this->emailType = EmailType::create()); return $this->emailType ?: ($this->emailType = new EmailType);
} }
/** /**
* @return UrlType * @return UrlTypeDefinition
*/ */
public function url() 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 // 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 * @return ListOfType
*/ */
public function listOf($type) public function listOf($type)
@ -155,7 +201,7 @@ class TypeSystem
} }
/** /**
* @param $type * @param Type|DefinitionContainer $type
* @return NonNull * @return NonNull
*/ */
public function nonNull($type) public function nonNull($type)

View File

@ -1,29 +1,45 @@
## Blog Example ## 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. ### Run locally
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
``` ```
php -S localhost:8080 ./index.php 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 ### Browsing API
The most convenient way to browse GraphQL API is by using [GraphiQL](https://github.com/graphql/graphiql) 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 But setting it up from scratch may be inconvenient. An easy alternative is to use one of
existing Google Chrome extensions: the existing Google Chrome extensions:
- [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij) - [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
- [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp) - [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 ### Running GraphQL queries
execute following query: Copy following query to GraphiQL and execute (by clicking play button on top bar)
``` ```
{ {
@ -31,9 +47,21 @@ execute following query:
id id
email email
} }
user(id: "2") {
id
email
}
stories(after: "1") {
id
body
comments {
...CommentView
}
}
lastStoryPosted { lastStoryPosted {
id id
isLiked hasViewerLiked
author { author {
id id
photo(size: ICON) { photo(size: ICON) {
@ -43,19 +71,49 @@ execute following query:
size size
width width
height height
# Uncomment to see error reporting for failed resolvers # Uncomment following line to see validation error:
# error # nonExistingField
# Uncomment to see error reporting for fields with exceptions thrown in resolvers
# fieldWithError
# nonNullFieldWithError
} }
lastStoryPosted { lastStoryPosted {
id id
} }
} }
body(format: HTML, maxLength: 10)
}
}
fragment CommentView on Comment {
id
body
totalReplyCount
replies {
id
body
} }
} }
``` ```
### Debugging ### Run your own query
By default this example runs in production mode without additional debugging tools enabled. 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, Note: GraphQL query requires at least one field per object type (to prevent accidental overfetching).
PHP errors handling and reporting - change your endpoint to `http://localhost:8080?debug=1` 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\Schema;
use \GraphQL\GraphQL; use \GraphQL\GraphQL;
use \GraphQL\Type\Definition\Config; use \GraphQL\Type\Definition\Config;
use \GraphQL\Error\FormattedError;
// Disable default PHP error reporting - we have better one for debug mode (see bellow) // Disable default PHP error reporting - we have better one for debug mode (see bellow)
ini_set('display_errors', 0); ini_set('display_errors', 0);
@ -49,6 +50,12 @@ try {
} }
$data += ['query' => null, 'variables' => null]; $data += ['query' => null, 'variables' => null];
if (null === $data['query']) {
$data['query'] = '
{hello}
';
}
// GraphQL schema to be passed to query executor: // GraphQL schema to be passed to query executor:
$schema = new Schema([ $schema = new Schema([
'query' => $typeSystem->query() 'query' => $typeSystem->query()
@ -73,9 +80,9 @@ try {
} catch (\Exception $error) { } catch (\Exception $error) {
$httpStatus = 500; $httpStatus = 500;
if (!empty($_GET['debug'])) { if (!empty($_GET['debug'])) {
$result['extensions']['exception'] = \GraphQL\Error\FormattedError::createFromException($error); $result['extensions']['exception'] = FormattedError::createFromException($error);
} else { } else {
$result['errors'] = \GraphQL\Error\FormattedError::create('Unexpected Error'); $result['errors'] = [FormattedError::create('Unexpected Error')];
} }
} }