This commit is contained in:
Kilian Hofmann
2021-06-01 19:56:34 +02:00
parent e74df463f2
commit 499abe195e
284 changed files with 55166 additions and 0 deletions
@@ -0,0 +1,18 @@
# Hello world
Clean and simple single-file example of main GraphQL concepts originally proposed and
implemented by [Leo Cavalcante](https://github.com/leocavalcante)
### Run locally
```
php -S localhost:8080 ./graphql.php
```
### Try query
```
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
```
### Try mutation
```
curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
```
@@ -0,0 +1,69 @@
<?php
// Test this using following command
// php -S localhost:8080 ./graphql.php &
// curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
// curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
require_once __DIR__ . '/../../vendor/autoload.php';
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\GraphQL;
try {
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'echo' => [
'type' => Type::string(),
'args' => [
'message' => ['type' => Type::string()],
],
'resolve' => function ($root, $args) {
return $root['prefix'] . $args['message'];
}
],
],
]);
$mutationType = new ObjectType([
'name' => 'Calc',
'fields' => [
'sum' => [
'type' => Type::int(),
'args' => [
'x' => ['type' => Type::int()],
'y' => ['type' => Type::int()],
],
'resolve' => function ($root, $args) {
return $args['x'] + $args['y'];
},
],
],
]);
// See docs on schema options:
// http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
]);
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;
$rootValue = ['prefix' => 'You said: '];
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
$output = $result->toArray();
} catch (\Exception $e) {
$output = [
'error' => [
'message' => $e->getMessage()
]
];
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($output);
@@ -0,0 +1,28 @@
<?php
namespace GraphQL\Examples\Blog;
use GraphQL\Examples\Blog\Data\User;
/**
* Class AppContext
* Instance available in all GraphQL resolvers as 3rd argument
*
* @package GraphQL\Examples\Blog
*/
class AppContext
{
/**
* @var string
*/
public $rootUrl;
/**
* @var User
*/
public $viewer;
/**
* @var \mixed
*/
public $request;
}
@@ -0,0 +1,25 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class Comment
{
public $id;
public $authorId;
public $storyId;
public $parentId;
public $body;
public $isAnonymous;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}
@@ -0,0 +1,206 @@
<?php
namespace GraphQL\Examples\Blog\Data;
/**
* Class DataSource
*
* This is just a simple in-memory data holder for the sake of example.
* Data layer for real app may use Doctrine or query the database directly (e.g. in CQRS style)
*
* @package GraphQL\Examples\Blog
*/
class DataSource
{
private static $users = [];
private static $stories = [];
private static $storyLikes = [];
private static $comments = [];
private static $storyComments = [];
private static $commentReplies = [];
private static $storyMentions = [];
public static function init()
{
self::$users = [
'1' => new User([
'id' => '1',
'email' => 'john@example.com',
'firstName' => 'John',
'lastName' => 'Doe'
]),
'2' => new User([
'id' => '2',
'email' => 'jane@example.com',
'firstName' => 'Jane',
'lastName' => 'Doe'
]),
'3' => new User([
'id' => '3',
'email' => 'john@example.com',
'firstName' => 'John',
'lastName' => 'Doe'
]),
];
self::$stories = [
'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"]),
];
self::$storyLikes = [
'1' => ['1', '2', '3'],
'2' => [],
'3' => ['1']
];
self::$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!']),
];
self::$storyComments = [
'1' => ['100', '200', '300'],
'2' => ['400', '500']
];
self::$commentReplies = [
'100' => ['110', '120', '130'],
'110' => ['111', '112', '113', '114', '115', '116', '117'],
];
self::$storyMentions = [
'1' => [
self::$users['2']
],
'2' => [
self::$stories['1'],
self::$users['3']
]
];
}
public static function findUser($id)
{
return isset(self::$users[$id]) ? self::$users[$id] : null;
}
public static function findStory($id)
{
return isset(self::$stories[$id]) ? self::$stories[$id] : null;
}
public static function findComment($id)
{
return isset(self::$comments[$id]) ? self::$comments[$id] : null;
}
public static function findLastStoryFor($authorId)
{
$storiesFound = array_filter(self::$stories, function(Story $story) use ($authorId) {
return $story->authorId == $authorId;
});
return !empty($storiesFound) ? $storiesFound[count($storiesFound) - 1] : null;
}
public static function findLikes($storyId, $limit)
{
$likes = isset(self::$storyLikes[$storyId]) ? self::$storyLikes[$storyId] : [];
$result = array_map(
function($userId) {
return self::$users[$userId];
},
$likes
);
return array_slice($result, 0, $limit);
}
public static function isLikedBy($storyId, $userId)
{
$subscribers = isset(self::$storyLikes[$storyId]) ? self::$storyLikes[$storyId] : [];
return in_array($userId, $subscribers);
}
public static function getUserPhoto($userId, $size)
{
return new Image([
'id' => $userId,
'type' => Image::TYPE_USERPIC,
'size' => $size,
'width' => rand(100, 200),
'height' => rand(100, 200)
]);
}
public static function findLatestStory()
{
return array_pop(self::$stories);
}
public static function findStories($limit, $afterId = null)
{
$start = $afterId ? (int) array_search($afterId, array_keys(self::$stories)) + 1 : 0;
return array_slice(array_values(self::$stories), $start, $limit);
}
public static function findComments($storyId, $limit = 5, $afterId = null)
{
$storyComments = isset(self::$storyComments[$storyId]) ? self::$storyComments[$storyId] : [];
$start = isset($after) ? (int) array_search($afterId, $storyComments) + 1 : 0;
$storyComments = array_slice($storyComments, $start, $limit);
return array_map(
function($commentId) {
return self::$comments[$commentId];
},
$storyComments
);
}
public static function findReplies($commentId, $limit = 5, $afterId = null)
{
$commentReplies = isset(self::$commentReplies[$commentId]) ? self::$commentReplies[$commentId] : [];
$start = isset($after) ? (int) array_search($afterId, $commentReplies) + 1: 0;
$commentReplies = array_slice($commentReplies, $start, $limit);
return array_map(
function($replyId) {
return self::$comments[$replyId];
},
$commentReplies
);
}
public static function countComments($storyId)
{
return isset(self::$storyComments[$storyId]) ? count(self::$storyComments[$storyId]) : 0;
}
public static function countReplies($commentId)
{
return isset(self::$commentReplies[$commentId]) ? count(self::$commentReplies[$commentId]) : 0;
}
public static function findStoryMentions($storyId)
{
return isset(self::$storyMentions[$storyId]) ? self::$storyMentions[$storyId] :[];
}
}
@@ -0,0 +1,29 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class Image
{
const TYPE_USERPIC = 'userpic';
const SIZE_ICON = 'icon';
const SIZE_SMALL = 'small';
const SIZE_MEDIUM = 'medium';
const SIZE_ORIGINAL = 'original';
public $id;
public $type;
public $size;
public $width;
public $height;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}
@@ -0,0 +1,22 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class Story
{
public $id;
public $authorId;
public $title;
public $body;
public $isAnonymous = false;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}
@@ -0,0 +1,22 @@
<?php
namespace GraphQL\Examples\Blog\Data;
use GraphQL\Utils\Utils;
class User
{
public $id;
public $email;
public $firstName;
public $lastName;
public $hasPhoto;
public function __construct(array $data)
{
Utils::assign($this, $data);
}
}
@@ -0,0 +1,76 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\Comment;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
class CommentType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'Comment',
'fields' => function() {
return [
'id' => Types::id(),
'author' => Types::user(),
'parent' => Types::comment(),
'isAnonymous' => Types::boolean(),
'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) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($value, $args, $context, $info);
} else {
return $value->{$info->fieldName};
}
}
];
parent::__construct($config);
}
public function resolveAuthor(Comment $comment)
{
if ($comment->isAnonymous) {
return null;
}
return DataSource::findUser($comment->authorId);
}
public function resolveParent(Comment $comment)
{
if ($comment->parentId) {
return DataSource::findComment($comment->parentId);
}
return null;
}
public function resolveReplies(Comment $comment, $args)
{
$args += ['after' => null];
return DataSource::findReplies($comment->id, $args['limit'], $args['after']);
}
public function resolveTotalReplyCount(Comment $comment)
{
return DataSource::countReplies($comment->id);
}
}
@@ -0,0 +1,19 @@
<?php
namespace GraphQL\Examples\Blog\Type\Enum;
use GraphQL\Type\Definition\EnumType;
class ContentFormatEnum extends EnumType
{
const FORMAT_TEXT = 'TEXT';
const FORMAT_HTML = 'HTML';
public function __construct()
{
$config = [
'name' => 'ContentFormatEnum',
'values' => [self::FORMAT_TEXT, self::FORMAT_HTML]
];
parent::__construct($config);
}
}
@@ -0,0 +1,23 @@
<?php
namespace GraphQL\Examples\Blog\Type\Enum;
use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Type\Definition\EnumType;
class ImageSizeEnumType extends EnumType
{
public function __construct()
{
$config = [
// Note: 'name' option is not needed in this form - it will be inferred from className
'values' => [
'ICON' => Image::SIZE_ICON,
'SMALL' => Image::SIZE_SMALL,
'MEDIUM' => Image::SIZE_MEDIUM,
'ORIGINAL' => Image::SIZE_ORIGINAL
]
];
parent::__construct($config);
}
}
@@ -0,0 +1,52 @@
<?php
namespace GraphQL\Examples\Blog\Type\Field;
use GraphQL\Examples\Blog\Type\Enum\ContentFormatEnum;
use GraphQL\Examples\Blog\Types;
class HtmlField
{
public static function build($name, $objectKey = null)
{
$objectKey = $objectKey ?: $name;
// Demonstrates how to organize re-usable fields
// Usual example: when the same field with same args shows up in different types
// (for example when it is a part of some interface)
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;
}
}
];
}
}
@@ -0,0 +1,62 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
class ImageType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'ImageType',
'fields' => [
'id' => Types::id(),
'type' => new EnumType([
'name' => 'ImageTypeEnum',
'values' => [
'USERPIC' => Image::TYPE_USERPIC
]
]),
'size' => Types::imageSizeEnum(),
'width' => Types::int(),
'height' => Types::int(),
'url' => [
'type' => Types::url(),
'resolve' => [$this, 'resolveUrl']
],
// Just for the sake of example
'fieldWithError' => [
'type' => Types::string(),
'resolve' => function() {
throw new \Exception("Field with exception");
}
],
'nonNullFieldWithError' => [
'type' => Types::nonNull(Types::string()),
'resolve' => function() {
throw new \Exception("Non-null field with exception");
}
]
]
];
parent::__construct($config);
}
public function resolveUrl(Image $value, $args, AppContext $context)
{
switch ($value->type) {
case Image::TYPE_USERPIC:
$path = "/images/user/{$value->id}-{$value->size}.jpg";
break;
default:
throw new \UnexpectedValueException("Unexpected image type: " . $value->type);
}
return $context->rootUrl . $path;
}
}
@@ -0,0 +1,34 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Data\Image;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\InterfaceType;
class NodeType extends InterfaceType
{
public function __construct()
{
$config = [
'name' => 'Node',
'fields' => [
'id' => Types::id()
],
'resolveType' => [$this, 'resolveNodeType']
];
parent::__construct($config);
}
public function resolveNodeType($object)
{
if ($object instanceof User) {
return Types::user();
} else if ($object instanceof Image) {
return Types::image();
} else if ($object instanceof Story) {
return Types::story();
}
}
}
@@ -0,0 +1,97 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
class QueryType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'Query',
'fields' => [
'user' => [
'type' => Types::user(),
'description' => 'Returns user by id (in range of 1-5)',
'args' => [
'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(),
'description' => 'Fetch stories listed after the story with this ID'
],
'limit' => [
'type' => Types::int(),
'description' => 'Number of stories to be returned',
'defaultValue' => 10
]
]
],
'lastStoryPosted' => [
'type' => Types::story(),
'description' => 'Returns last story posted for this blog'
],
'deprecatedField' => [
'type' => Types::string(),
'deprecationReason' => 'This field is deprecated!'
],
'fieldWithException' => [
'type' => Types::string(),
'resolve' => function() {
throw new \Exception("Exception message thrown in field resolver");
}
],
'hello' => Type::string()
],
'resolveField' => function($val, $args, $context, ResolveInfo $info) {
return $this->{$info->fieldName}($val, $args, $context, $info);
}
];
parent::__construct($config);
}
public function user($rootValue, $args)
{
return DataSource::findUser($args['id']);
}
public function viewer($rootValue, $args, AppContext $context)
{
return $context->viewer;
}
public function stories($rootValue, $args)
{
$args += ['after' => null];
return DataSource::findStories($args['limit'], $args['after']);
}
public function lastStoryPosted()
{
return 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.';
}
}
@@ -0,0 +1,70 @@
<?php
namespace GraphQL\Examples\Blog\Type\Scalar;
use GraphQL\Error\Error;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Utils\Utils;
class EmailType
{
public static function create()
{
return new CustomScalarType([
'name' => 'Email',
'serialize' => [__CLASS__, 'serialize'],
'parseValue' => [__CLASS__, 'parseValue'],
'parseLiteral' => [__CLASS__, 'parseLiteral'],
]);
}
/**
* Serializes an internal value to include in a response.
*
* @param string $value
* @return string
*/
public static function serialize($value)
{
// Assuming internal representation of email is always correct:
return $value;
// If it might be incorrect and you want to make sure that only correct values are included in response -
// use following line instead:
// return $this->parseValue($value);
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
*/
public static function parseValue($value)
{
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value));
}
return $value;
}
/**
* Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
*
* @param \GraphQL\Language\AST\Node $valueNode
* @return string
* @throws Error
*/
public static function parseLiteral($valueNode)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
if (!$valueNode instanceof StringValueNode) {
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
}
if (!filter_var($valueNode->value, FILTER_VALIDATE_EMAIL)) {
throw new Error("Not a valid email", [$valueNode]);
}
return $valueNode->value;
}
}
@@ -0,0 +1,63 @@
<?php
namespace GraphQL\Examples\Blog\Type\Scalar;
use GraphQL\Error\Error;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\ScalarType;
use GraphQL\Utils\Utils;
class UrlType extends ScalarType
{
/**
* Serializes an internal value to include in a response.
*
* @param mixed $value
* @return mixed
*/
public function serialize($value)
{
// Assuming internal representation of url is always correct:
return $value;
// If it might be incorrect and you want to make sure that only correct values are included in response -
// use following line instead:
// return $this->parseValue($value);
}
/**
* Parses an externally provided value (query variable) to use as an input
*
* @param mixed $value
* @return mixed
* @throws Error
*/
public function parseValue($value)
{
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example
throw new Error("Cannot represent value as URL: " . Utils::printSafe($value));
}
return $value;
}
/**
* Parses an externally provided literal value to use as an input (e.g. in Query AST)
*
* @param Node $valueNode
* @param array|null $variables
* @return null|string
* @throws Error
*/
public function parseLiteral($valueNode, array $variables = null)
{
// Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
// error location in query:
if (!($valueNode instanceof StringValueNode)) {
throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
}
if (!is_string($valueNode->value) || !filter_var($valueNode->value, FILTER_VALIDATE_URL)) {
throw new Error('Query error: Not a valid URL', [$valueNode]);
}
return $valueNode->value;
}
}
@@ -0,0 +1,31 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\UnionType;
class SearchResultType extends UnionType
{
public function __construct()
{
$config = [
'name' => 'SearchResultType',
'types' => function() {
return [
Types::story(),
Types::user()
];
},
'resolveType' => function($value) {
if ($value instanceof Story) {
return Types::story();
} else if ($value instanceof User) {
return Types::user();
}
}
];
parent::__construct($config);
}
}
@@ -0,0 +1,127 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Data\Story;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Class StoryType
* @package GraphQL\Examples\Social\Type
*/
class StoryType extends ObjectType
{
const EDIT = 'EDIT';
const DELETE = 'DELETE';
const LIKE = 'LIKE';
const UNLIKE = 'UNLIKE';
const REPLY = 'REPLY';
public function __construct()
{
$config = [
'name' => 'Story',
'fields' => function() {
return [
'id' => Types::id(),
'author' => Types::user(),
'mentions' => Types::listOf(Types::mention()),
'totalCommentCount' => Types::int(),
'comments' => [
'type' => Types::listOf(Types::comment()),
'args' => [
'after' => [
'type' => Types::id(),
'description' => 'Load all comments listed after given comment ID'
],
'limit' => [
'type' => Types::int(),
'defaultValue' => 5
]
]
],
'likes' => [
'type' => Types::listOf(Types::user()),
'args' => [
'limit' => [
'type' => Types::int(),
'description' => 'Limit the number of recent likes returned',
'defaultValue' => 5
]
]
],
'likedBy' => [
'type' => Types::listOf(Types::user()),
],
'affordances' => Types::listOf(new EnumType([
'name' => 'StoryAffordancesEnum',
'values' => [
self::EDIT,
self::DELETE,
self::LIKE,
self::UNLIKE,
self::REPLY
]
])),
'hasViewerLiked' => Types::boolean(),
Types::htmlField('body'),
];
},
'interfaces' => [
Types::node()
],
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($value, $args, $context, $info);
} else {
return $value->{$info->fieldName};
}
}
];
parent::__construct($config);
}
public function resolveAuthor(Story $story)
{
return DataSource::findUser($story->authorId);
}
public function resolveAffordances(Story $story, $args, AppContext $context)
{
$isViewer = $context->viewer === DataSource::findUser($story->authorId);
$isLiked = DataSource::isLikedBy($story->id, $context->viewer->id);
if ($isViewer) {
$affordances[] = self::EDIT;
$affordances[] = self::DELETE;
}
if ($isLiked) {
$affordances[] = self::UNLIKE;
} else {
$affordances[] = self::LIKE;
}
return $affordances;
}
public function resolveHasViewerLiked(Story $story, $args, AppContext $context)
{
return DataSource::isLikedBy($story->id, $context->viewer->id);
}
public function resolveTotalCommentCount(Story $story)
{
return DataSource::countComments($story->id);
}
public function resolveComments(Story $story, $args)
{
$args += ['after' => null];
return DataSource::findComments($story->id, $args['limit'], $args['after']);
}
}
@@ -0,0 +1,68 @@
<?php
namespace GraphQL\Examples\Blog\Type;
use GraphQL\Examples\Blog\AppContext;
use GraphQL\Examples\Blog\Data\DataSource;
use GraphQL\Examples\Blog\Data\User;
use GraphQL\Examples\Blog\Types;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
class UserType extends ObjectType
{
public function __construct()
{
$config = [
'name' => 'User',
'description' => 'Our blog authors',
'fields' => function() {
return [
'id' => Types::id(),
'email' => Types::email(),
'photo' => [
'type' => Types::image(),
'description' => 'User photo URL',
'args' => [
'size' => Types::nonNull(Types::imageSizeEnum()),
]
],
'firstName' => [
'type' => Types::string(),
],
'lastName' => [
'type' => Types::string(),
],
'lastStoryPosted' => Types::story(),
'fieldWithError' => [
'type' => Types::string(),
'resolve' => function() {
throw new \Exception("This is error field");
}
]
];
},
'interfaces' => [
Types::node()
],
'resolveField' => function($value, $args, $context, ResolveInfo $info) {
$method = 'resolve' . ucfirst($info->fieldName);
if (method_exists($this, $method)) {
return $this->{$method}($value, $args, $context, $info);
} else {
return $value->{$info->fieldName};
}
}
];
parent::__construct($config);
}
public function resolvePhoto(User $user, $args)
{
return DataSource::getUserPhoto($user->id, $args['size']);
}
public function resolveLastStoryPosted(User $user)
{
return DataSource::findLastStoryFor($user->id);
}
}
@@ -0,0 +1,209 @@
<?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\SearchResultType;
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\UserType;
use GraphQL\Examples\Blog\Type\ImageType;
use GraphQL\Type\Definition\ListOfType;
use GraphQL\Type\Definition\NonNull;
use GraphQL\Type\Definition\Type;
/**
* Class Types
*
* 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).
*
* @package GraphQL\Examples\Blog
*/
class Types
{
// Object types:
private static $user;
private static $story;
private static $comment;
private static $image;
private static $query;
/**
* @return UserType
*/
public static function user()
{
return self::$user ?: (self::$user = new UserType());
}
/**
* @return StoryType
*/
public static function story()
{
return self::$story ?: (self::$story = new StoryType());
}
/**
* @return CommentType
*/
public static function comment()
{
return self::$comment ?: (self::$comment = new CommentType());
}
/**
* @return ImageType
*/
public static function image()
{
return self::$image ?: (self::$image = new ImageType());
}
/**
* @return QueryType
*/
public static function query()
{
return self::$query ?: (self::$query = new QueryType());
}
// Interface types
private static $node;
/**
* @return NodeType
*/
public static function node()
{
return self::$node ?: (self::$node = new NodeType());
}
// Unions types:
private static $mention;
/**
* @return SearchResultType
*/
public static function mention()
{
return self::$mention ?: (self::$mention = new SearchResultType());
}
// Enum types
private static $imageSizeEnum;
private static $contentFormatEnum;
/**
* @return ImageSizeEnumType
*/
public static function imageSizeEnum()
{
return self::$imageSizeEnum ?: (self::$imageSizeEnum = new ImageSizeEnumType());
}
/**
* @return ContentFormatEnum
*/
public static function contentFormatEnum()
{
return self::$contentFormatEnum ?: (self::$contentFormatEnum = new ContentFormatEnum());
}
// Custom Scalar types:
private static $urlType;
private static $emailType;
public static function email()
{
return self::$emailType ?: (self::$emailType = EmailType::create());
}
/**
* @return UrlType
*/
public static function url()
{
return self::$urlType ?: (self::$urlType = new UrlType());
}
/**
* @param $name
* @param null $objectKey
* @return array
*/
public static function htmlField($name, $objectKey = null)
{
return HtmlField::build($name, $objectKey);
}
// Let's add internal types as well for consistent experience
public static function boolean()
{
return Type::boolean();
}
/**
* @return \GraphQL\Type\Definition\FloatType
*/
public static function float()
{
return Type::float();
}
/**
* @return \GraphQL\Type\Definition\IDType
*/
public static function id()
{
return Type::id();
}
/**
* @return \GraphQL\Type\Definition\IntType
*/
public static function int()
{
return Type::int();
}
/**
* @return \GraphQL\Type\Definition\StringType
*/
public static function string()
{
return Type::string();
}
/**
* @param Type $type
* @return ListOfType
*/
public static function listOf($type)
{
return new ListOfType($type);
}
/**
* @param Type $type
* @return NonNull
*/
public static function nonNull($type)
{
return new NonNull($type);
}
}
+120
View File
@@ -0,0 +1,120 @@
## Blog Example
Simple yet full-featured example of GraphQL API. Models blogging platform with Stories, Users
and hierarchical comments.
### Run locally
```
php -S localhost:8080 ./graphql.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. 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)
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.
### Running GraphQL queries
Copy following query to GraphiQL and execute (by clicking play button on top bar)
```
{
viewer {
id
email
}
user(id: "2") {
id
email
}
stories(after: "1") {
id
body
comments {
...CommentView
}
}
lastStoryPosted {
id
hasViewerLiked
author {
id
photo(size: ICON) {
id
url
type
size
width
height
# 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
}
}
```
### Run your own query
Use GraphiQL autocomplete (via CTRL+space) to easily create your own query.
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.
+71
View File
@@ -0,0 +1,71 @@
<?php
// Test this using following command
// php -S localhost:8080 ./graphql.php
require_once __DIR__ . '/../../vendor/autoload.php';
use \GraphQL\Examples\Blog\Types;
use \GraphQL\Examples\Blog\AppContext;
use \GraphQL\Examples\Blog\Data\DataSource;
use \GraphQL\Type\Schema;
use \GraphQL\GraphQL;
use \GraphQL\Error\FormattedError;
use \GraphQL\Error\Debug;
// Disable default PHP error reporting - we have better one for debug mode (see bellow)
ini_set('display_errors', 0);
$debug = false;
if (!empty($_GET['debug'])) {
set_error_handler(function($severity, $message, $file, $line) use (&$phpErrors) {
throw new ErrorException($message, 0, $severity, $file, $line);
});
$debug = Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
}
try {
// Initialize our fake data source
DataSource::init();
// Prepare context that will be available in all field resolvers (as 3rd argument):
$appContext = new AppContext();
$appContext->viewer = DataSource::findUser('1'); // simulated "currently logged-in user"
$appContext->rootUrl = 'http://localhost:8080';
$appContext->request = $_REQUEST;
// Parse incoming query and variables
if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) {
$raw = file_get_contents('php://input') ?: '';
$data = json_decode($raw, true) ?: [];
} else {
$data = $_REQUEST;
}
$data += ['query' => null, 'variables' => null];
if (null === $data['query']) {
$data['query'] = '{hello}';
}
// GraphQL schema to be passed to query executor:
$schema = new Schema([
'query' => Types::query()
]);
$result = GraphQL::executeQuery(
$schema,
$data['query'],
null,
$appContext,
(array) $data['variables']
);
$output = $result->toArray($debug);
$httpStatus = 200;
} catch (\Exception $error) {
$httpStatus = 500;
$output['errors'] = [
FormattedError::createFromException($error, $debug)
];
}
header('Content-Type: application/json', true, $httpStatus);
echo json_encode($output);
@@ -0,0 +1,19 @@
# Parsing GraphQL IDL shorthand
Same as the Hello world example but shows how to build GraphQL schema from shorthand
and wire up some resolvers
### Run locally
```
php -S localhost:8080 ./graphql.php
```
### Try query
```
curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
```
### Try mutation
```
curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
```
@@ -0,0 +1,31 @@
<?php
// Test this using following command
// php -S localhost:8080 ./graphql.php &
// curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
// curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
require_once __DIR__ . '/../../vendor/autoload.php';
use GraphQL\GraphQL;
use GraphQL\Utils\BuildSchema;
try {
$schema = BuildSchema::build(file_get_contents(__DIR__ . '/schema.graphqls'));
$rootValue = include __DIR__ . '/rootvalue.php';
$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);
$query = $input['query'];
$variableValues = isset($input['variables']) ? $input['variables'] : null;
$result = GraphQL::executeQuery($schema, $query, $rootValue, null, $variableValues);
} catch (\Exception $e) {
$result = [
'error' => [
'message' => $e->getMessage()
]
];
}
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);
@@ -0,0 +1,35 @@
<?php
interface Resolver {
public function resolve($root, $args, $context);
}
class Addition implements Resolver
{
public function resolve($root, $args, $context)
{
return $args['x'] + $args['y'];
}
}
class Echoer implements Resolver
{
public function resolve($root, $args, $context)
{
return $root['prefix'].$args['message'];
}
}
return [
'sum' => function($root, $args, $context) {
$sum = new Addition();
return $sum->resolve($root, $args, $context);
},
'echo' => function($root, $args, $context) {
$echo = new Echoer();
return $echo->resolve($root, $args, $context);
},
'prefix' => 'You said: ',
];
@@ -0,0 +1,13 @@
schema {
query: Query
mutation: Calc
}
type Calc {
sum(x: Int, y: Int): Int
}
type Query {
echo(message: String): String
}
+19
View File
@@ -0,0 +1,19 @@
# Hello world
Same example as 01-hello-world, but uses
[Standard Http Server](http://webonyx.github.io/graphql-php/executing-queries/#using-server)
instead of manual parsing of incoming data.
### Run locally
```
php -S localhost:8080 ./graphql.php
```
### Try query
```
curl -d '{"query": "query { echo(message: \"Hello World\") }" }' -H "Content-Type: application/json" http://localhost:8080
```
### Try mutation
```
curl -d '{"query": "mutation { sum(x: 2, y: 2) }" }' -H "Content-Type: application/json" http://localhost:8080
```
@@ -0,0 +1,61 @@
<?php
// Test this using following command
// php -S localhost:8080 ./graphql.php &
// curl http://localhost:8080 -d '{"query": "query { echo(message: \"Hello World\") }" }'
// curl http://localhost:8080 -d '{"query": "mutation { sum(x: 2, y: 2) }" }'
require_once __DIR__ . '/../../vendor/autoload.php';
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
use GraphQL\Server\StandardServer;
try {
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'echo' => [
'type' => Type::string(),
'args' => [
'message' => ['type' => Type::string()],
],
'resolve' => function ($root, $args) {
return $root['prefix'] . $args['message'];
}
],
],
]);
$mutationType = new ObjectType([
'name' => 'Calc',
'fields' => [
'sum' => [
'type' => Type::int(),
'args' => [
'x' => ['type' => Type::int()],
'y' => ['type' => Type::int()],
],
'resolve' => function ($root, $args) {
return $args['x'] + $args['y'];
},
],
],
]);
// See docs on schema options:
// http://webonyx.github.io/graphql-php/type-system/schema/#configuration-options
$schema = new Schema([
'query' => $queryType,
'mutation' => $mutationType,
]);
// See docs on server options:
// http://webonyx.github.io/graphql-php/executing-queries/#server-configuration-options
$server = new StandardServer([
'schema' => $schema
]);
$server->handleRequest();
} catch (\Exception $e) {
StandardServer::send500Error($e);
}