mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-28 15:56:02 +03:00
Updated blog example
This commit is contained in:
parent
2ef58a615f
commit
85d2c2cef3
25
examples/01-blog/Blog/Data/Comment.php
Normal file
25
examples/01-blog/Blog/Data/Comment.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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] :[];
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,6 @@ class Story
|
||||
|
||||
public $isAnonymous = false;
|
||||
|
||||
public $isLiked = false;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
Utils::assign($this, $data);
|
||||
|
21
examples/01-blog/Blog/Type/BaseType.php
Normal file
21
examples/01-blog/Blog/Type/BaseType.php
Normal 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;
|
||||
}
|
||||
}
|
61
examples/01-blog/Blog/Type/CommentType.php
Normal file
61
examples/01-blog/Blog/Type/CommentType.php
Normal 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);
|
||||
}
|
||||
}
|
19
examples/01-blog/Blog/Type/Enum/ContentFormatEnum.php
Normal file
19
examples/01-blog/Blog/Type/Enum/ContentFormatEnum.php
Normal 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]
|
||||
]);
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
49
examples/01-blog/Blog/Type/Field/HtmlField.php
Normal file
49
examples/01-blog/Blog/Type/Field/HtmlField.php
Normal 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;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
31
examples/01-blog/Blog/Type/FieldDefinitions.php
Normal file
31
examples/01-blog/Blog/Type/FieldDefinitions.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
]
|
||||
]
|
||||
|
31
examples/01-blog/Blog/Type/MentionType.php
Normal file
31
examples/01-blog/Blog/Type/MentionType.php
Normal 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();
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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.';
|
||||
}
|
||||
}
|
||||
|
@ -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'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*
|
@ -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']);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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')];
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user