first commit

This commit is contained in:
2024-10-28 22:14:22 +01:00
commit b65352c452
40581 changed files with 5712079 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
drwxr-xr-x 3 30094 users 6 Oct 6 10:16 .
drwxr-xr-x 6 30094 users 7 Oct 6 10:16 ..
drwxr-xr-x 4 30094 users 7 Oct 6 10:16 Blog
-rw-r--r-- 1 30094 users 2954 Aug 31 2021 README.md
-rw-r--r-- 1 30094 users 2156 Aug 31 2021 graphql.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

@@ -0,0 +1,7 @@
drwxr-xr-x 4 30094 users 7 Oct 6 10:16 .
drwxr-xr-x 3 30094 users 6 Oct 6 10:16 ..
-rw-r--r-- 1 30094 users 394 Aug 31 2021 AppContext.php
drwxr-xr-x 2 30094 users 8 Oct 6 10:16 Data
drwxr-xr-x 5 30094 users 13 Oct 6 10:16 Type
-rw-r--r-- 1 30094 users 4428 Aug 31 2021 Types.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

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

View File

@@ -0,0 +1,8 @@
drwxr-xr-x 2 30094 users 8 Oct 6 10:16 .
drwxr-xr-x 4 30094 users 7 Oct 6 10:16 ..
-rw-r--r-- 1 30094 users 314 Aug 31 2021 Comment.php
-rw-r--r-- 1 30094 users 7667 Aug 31 2021 DataSource.php
-rw-r--r-- 1 30094 users 449 Aug 31 2021 Image.php
-rw-r--r-- 1 30094 users 294 Aug 31 2021 Story.php
-rw-r--r-- 1 30094 users 287 Aug 31 2021 User.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

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

View File

@@ -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] :[];
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

@@ -0,0 +1,13 @@
drwxr-xr-x 5 30094 users 13 Oct 6 10:16 .
drwxr-xr-x 4 30094 users 7 Oct 6 10:16 ..
-rw-r--r-- 1 30094 users 2383 Aug 31 2021 CommentType.php
drwxr-xr-x 2 30094 users 5 Oct 6 10:16 Enum
drwxr-xr-x 2 30094 users 4 Oct 6 10:16 Field
-rw-r--r-- 1 30094 users 1982 Aug 31 2021 ImageType.php
-rw-r--r-- 1 30094 users 859 Aug 31 2021 NodeType.php
-rw-r--r-- 1 30094 users 3347 Aug 31 2021 QueryType.php
drwxr-xr-x 2 30094 users 5 Oct 6 10:16 Scalar
-rw-r--r-- 1 30094 users 825 Aug 31 2021 SearchResultType.php
-rw-r--r-- 1 30094 users 4310 Aug 31 2021 StoryType.php
-rw-r--r-- 1 30094 users 2264 Aug 31 2021 UserType.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

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

View File

@@ -0,0 +1,5 @@
drwxr-xr-x 2 30094 users 5 Oct 6 10:16 .
drwxr-xr-x 5 30094 users 13 Oct 6 10:16 ..
-rw-r--r-- 1 30094 users 420 Aug 31 2021 ContentFormatEnum.php
-rw-r--r-- 1 30094 users 616 Aug 31 2021 ImageSizeEnumType.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

@@ -0,0 +1,4 @@
drwxr-xr-x 2 30094 users 4 Oct 6 10:16 .
drwxr-xr-x 5 30094 users 13 Oct 6 10:16 ..
-rw-r--r-- 1 30094 users 1745 Aug 31 2021 HtmlField.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
drwxr-xr-x 2 30094 users 5 Oct 6 10:16 .
drwxr-xr-x 5 30094 users 13 Oct 6 10:16 ..
-rw-r--r-- 1 30094 users 2172 Aug 31 2021 EmailType.php
-rw-r--r-- 1 30094 users 2036 Aug 31 2021 UrlType.php
-rw-r--r-- 1 30094 users 298 Aug 31 2021 index.php

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

View File

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;

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.

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

View File

@@ -0,0 +1,11 @@
<?php
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
header("Location: ../");
exit;