Vendor
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
+23
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user