mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-25 14:26:08 +03:00
First official example that should help newcomers to start (incomplete yet, but still useful)
This commit is contained in:
parent
6c076e21d4
commit
d41687913a
@ -25,7 +25,8 @@
|
|||||||
"autoload-dev": {
|
"autoload-dev": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"GraphQL\\Tests\\": "tests/",
|
"GraphQL\\Tests\\": "tests/",
|
||||||
"GraphQL\\Benchmarks\\": "benchmarks/"
|
"GraphQL\\Benchmarks\\": "benchmarks/",
|
||||||
|
"GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
examples/01-blog/Blog/AppContext.php
Normal file
35
examples/01-blog/Blog/AppContext.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\Data\DataSource;
|
||||||
|
use GraphQL\Examples\Blog\Data\User;
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class AppContext
|
||||||
|
* Instance available in all GraphQL resolvers
|
||||||
|
*
|
||||||
|
* @package GraphQL\Examples\Social
|
||||||
|
*/
|
||||||
|
class AppContext
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $rootUrl;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var User
|
||||||
|
*/
|
||||||
|
public $viewer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \mixed
|
||||||
|
*/
|
||||||
|
public $request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DataSource
|
||||||
|
*/
|
||||||
|
public $dataSource;
|
||||||
|
}
|
95
examples/01-blog/Blog/Data/DataSource.php
Normal file
95
examples/01-blog/Blog/Data/DataSource.php
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
<?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 $users = [];
|
||||||
|
|
||||||
|
private $stories = [];
|
||||||
|
|
||||||
|
private $storyLikes = [];
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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'
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->stories = [
|
||||||
|
1 => new Story(['id' => 1, 'authorId' => 1]),
|
||||||
|
2 => new Story(['id' => 2, 'authorId' => 1]),
|
||||||
|
3 => new Story(['id' => 3, 'authorId' => 3]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->storyLikes = [
|
||||||
|
1 => [1, 2, 3],
|
||||||
|
2 => [],
|
||||||
|
3 => [1]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findUser($id)
|
||||||
|
{
|
||||||
|
return isset($this->users[$id]) ? $this->users[$id] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findStory($id)
|
||||||
|
{
|
||||||
|
return isset($this->stories[$id]) ? $this->stories[$id] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findLastStoryFor($authorId)
|
||||||
|
{
|
||||||
|
$storiesFound = array_filter($this->stories, function(Story $story) use ($authorId) {
|
||||||
|
return $story->authorId == $authorId;
|
||||||
|
});
|
||||||
|
return !empty($storiesFound) ? $storiesFound[count($storiesFound) - 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isLikedBy(Story $story, User $user)
|
||||||
|
{
|
||||||
|
$subscribers = isset($this->storyLikes[$story->id]) ? $this->storyLikes[$story->id] : [];
|
||||||
|
return in_array($user->id, $subscribers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUserPhoto(User $user, $size)
|
||||||
|
{
|
||||||
|
return new Image([
|
||||||
|
'id' => $user->id,
|
||||||
|
'type' => Image::TYPE_USERPIC,
|
||||||
|
'size' => $size,
|
||||||
|
'width' => rand(100, 200),
|
||||||
|
'height' => rand(100, 200)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findLatestStory()
|
||||||
|
{
|
||||||
|
return array_pop($this->stories);
|
||||||
|
}
|
||||||
|
}
|
29
examples/01-blog/Blog/Data/Image.php
Normal file
29
examples/01-blog/Blog/Data/Image.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Data;
|
||||||
|
|
||||||
|
use GraphQL\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);
|
||||||
|
}
|
||||||
|
}
|
24
examples/01-blog/Blog/Data/Story.php
Normal file
24
examples/01-blog/Blog/Data/Story.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Data;
|
||||||
|
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
|
class Story
|
||||||
|
{
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
public $authorId;
|
||||||
|
|
||||||
|
public $title;
|
||||||
|
|
||||||
|
public $body;
|
||||||
|
|
||||||
|
public $isAnonymous = false;
|
||||||
|
|
||||||
|
public $isLiked = false;
|
||||||
|
|
||||||
|
public function __construct(array $data)
|
||||||
|
{
|
||||||
|
Utils::assign($this, $data);
|
||||||
|
}
|
||||||
|
}
|
22
examples/01-blog/Blog/Data/User.php
Normal file
22
examples/01-blog/Blog/Data/User.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Data;
|
||||||
|
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
|
class User
|
||||||
|
{
|
||||||
|
public $id;
|
||||||
|
|
||||||
|
public $email;
|
||||||
|
|
||||||
|
public $firstName;
|
||||||
|
|
||||||
|
public $lastName;
|
||||||
|
|
||||||
|
public $hasPhoto;
|
||||||
|
|
||||||
|
public function __construct(array $data)
|
||||||
|
{
|
||||||
|
Utils::assign($this, $data);
|
||||||
|
}
|
||||||
|
}
|
24
examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php
Normal file
24
examples/01-blog/Blog/Type/Enum/ImageSizeEnumType.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type\Enum;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\Data\Image;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
|
||||||
|
class ImageSizeEnumType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return EnumType
|
||||||
|
*/
|
||||||
|
public static function getDefinition()
|
||||||
|
{
|
||||||
|
return new EnumType([
|
||||||
|
'name' => 'ImageSizeEnum',
|
||||||
|
'values' => [
|
||||||
|
'ICON' => Image::SIZE_ICON,
|
||||||
|
'SMALL' => Image::SIZE_SMALL,
|
||||||
|
'MEDIUM' => Image::SIZE_MEDIUM,
|
||||||
|
'ORIGINAL' => Image::SIZE_ORIGINAL
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
54
examples/01-blog/Blog/Type/ImageType.php
Normal file
54
examples/01-blog/Blog/Type/ImageType.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\AppContext;
|
||||||
|
use GraphQL\Examples\Blog\Data\Image;
|
||||||
|
use GraphQL\Examples\Blog\TypeSystem;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
|
||||||
|
class ImageType
|
||||||
|
{
|
||||||
|
public static function getDefinition(TypeSystem $types)
|
||||||
|
{
|
||||||
|
$handler = new self();
|
||||||
|
|
||||||
|
return new ObjectType([
|
||||||
|
'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' => [$handler, 'resolveUrl']
|
||||||
|
],
|
||||||
|
'error' => [
|
||||||
|
'type' => $types->string(),
|
||||||
|
'resolve' => function() {
|
||||||
|
throw new \Exception("This is error field");
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
39
examples/01-blog/Blog/Type/NodeType.php
Normal file
39
examples/01-blog/Blog/Type/NodeType.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\Data\Story;
|
||||||
|
use GraphQL\Examples\Blog\Data\User;
|
||||||
|
use GraphQL\Examples\Blog\Image;
|
||||||
|
use GraphQL\Examples\Blog\TypeSystem;
|
||||||
|
use GraphQL\Type\Definition\InterfaceType;
|
||||||
|
|
||||||
|
class NodeType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param TypeSystem $types
|
||||||
|
* @return InterfaceType
|
||||||
|
*/
|
||||||
|
public static function getDefinition(TypeSystem $types)
|
||||||
|
{
|
||||||
|
return new InterfaceType([
|
||||||
|
'name' => 'Node',
|
||||||
|
'fields' => [
|
||||||
|
'id' => $types->id()
|
||||||
|
],
|
||||||
|
'resolveType' => function ($object) use ($types) {
|
||||||
|
return self::resolveType($object, $types);
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function resolveType($object, TypeSystem $types)
|
||||||
|
{
|
||||||
|
if ($object instanceof User) {
|
||||||
|
return $types->user();
|
||||||
|
} else if ($object instanceof Image) {
|
||||||
|
return $types->image();
|
||||||
|
} else if ($object instanceof Story) {
|
||||||
|
return $types->story();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
examples/01-blog/Blog/Type/QueryType.php
Normal file
54
examples/01-blog/Blog/Type/QueryType.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\AppContext;
|
||||||
|
use GraphQL\Examples\Blog\TypeSystem;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\ResolveInfo;
|
||||||
|
|
||||||
|
class QueryType
|
||||||
|
{
|
||||||
|
public static function getDefinition(TypeSystem $types)
|
||||||
|
{
|
||||||
|
$handler = new self();
|
||||||
|
|
||||||
|
return new ObjectType([
|
||||||
|
'name' => 'Query',
|
||||||
|
'fields' => [
|
||||||
|
'user' => [
|
||||||
|
'type' => $types->user(),
|
||||||
|
'args' => [
|
||||||
|
'id' => [
|
||||||
|
'type' => $types->id(),
|
||||||
|
'defaultValue' => 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'viewer' => $types->user(),
|
||||||
|
'lastStoryPosted' => $types->story(),
|
||||||
|
'stories' => [
|
||||||
|
'type' => $types->listOf($types->story()),
|
||||||
|
'args' => []
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'resolveField' => function($val, $args, $context, ResolveInfo $info) use ($handler) {
|
||||||
|
return $handler->{$info->fieldName}($val, $args, $context, $info);
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user($val, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
return $context->dataSource->findUser($args['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function viewer($val, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
return $context->viewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastStoryPosted($val, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
return $context->dataSource->findLatestStory();
|
||||||
|
}
|
||||||
|
}
|
60
examples/01-blog/Blog/Type/Scalar/EmailType.php
Normal file
60
examples/01-blog/Blog/Type/Scalar/EmailType.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type\Scalar;
|
||||||
|
|
||||||
|
use GraphQL\Language\AST\StringValue;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
|
class EmailType extends ScalarType
|
||||||
|
{
|
||||||
|
public $name = 'Email';
|
||||||
|
|
||||||
|
public static function create()
|
||||||
|
{
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes an internal value to include in a response.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function serialize($value)
|
||||||
|
{
|
||||||
|
return $this->coerceEmail($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an externally provided value to use as an input
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function parseValue($value)
|
||||||
|
{
|
||||||
|
return $this->coerceEmail($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function coerceEmail($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 to use as an input
|
||||||
|
*
|
||||||
|
* @param \GraphQL\Language\AST\Value $valueAST
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function parseLiteral($valueAST)
|
||||||
|
{
|
||||||
|
if ($valueAST instanceof StringValue) {
|
||||||
|
return $valueAST->value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
64
examples/01-blog/Blog/Type/Scalar/UrlType.php
Normal file
64
examples/01-blog/Blog/Type/Scalar/UrlType.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type\Scalar;
|
||||||
|
|
||||||
|
use GraphQL\Language\AST\StringValue;
|
||||||
|
use GraphQL\Type\Definition\ScalarType;
|
||||||
|
use GraphQL\Utils;
|
||||||
|
|
||||||
|
class UrlType extends ScalarType
|
||||||
|
{
|
||||||
|
public $name = 'Url';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return UrlType
|
||||||
|
*/
|
||||||
|
public static function create()
|
||||||
|
{
|
||||||
|
return new self();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes an internal value to include in a response.
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function serialize($value)
|
||||||
|
{
|
||||||
|
return $this->coerceUrl($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an externally provided value to use as an input
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function parseValue($value)
|
||||||
|
{
|
||||||
|
return $this->coerceUrl($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $value
|
||||||
|
* @return float|null
|
||||||
|
*/
|
||||||
|
private function coerceUrl($value)
|
||||||
|
{
|
||||||
|
if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_URL)) { // quite naive, but after all this is example
|
||||||
|
throw new \UnexpectedValueException("Cannot represent value as URL: " . Utils::printSafe($value));
|
||||||
|
}
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return null|string
|
||||||
|
*/
|
||||||
|
public function parseLiteral($ast)
|
||||||
|
{
|
||||||
|
if ($ast instanceof StringValue) {
|
||||||
|
return $ast->value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
114
examples/01-blog/Blog/Type/StoryType.php
Normal file
114
examples/01-blog/Blog/Type/StoryType.php
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\AppContext;
|
||||||
|
use GraphQL\Examples\Blog\Data\Story;
|
||||||
|
use GraphQL\Examples\Blog\Data\User;
|
||||||
|
use GraphQL\Examples\Blog\TypeSystem;
|
||||||
|
use GraphQL\Type\Definition\EnumType;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\ResolveInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class StoryType
|
||||||
|
* @package GraphQL\Examples\Social\Type
|
||||||
|
*/
|
||||||
|
class StoryType
|
||||||
|
{
|
||||||
|
const EDIT = 'EDIT';
|
||||||
|
const DELETE = 'DELETE';
|
||||||
|
const LIKE = 'LIKE';
|
||||||
|
const UNLIKE = 'UNLIKE';
|
||||||
|
|
||||||
|
const FORMAT_TEXT = 'TEXT';
|
||||||
|
const FORMAT_HTML = 'HTML';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TypeSystem $types
|
||||||
|
* @return ObjectType
|
||||||
|
*/
|
||||||
|
public static function getDefinition(TypeSystem $types)
|
||||||
|
{
|
||||||
|
// Type instance containing resolvers for field definitions
|
||||||
|
$handler = new self();
|
||||||
|
|
||||||
|
// Return definition for this type:
|
||||||
|
return new ObjectType([
|
||||||
|
'name' => 'Story',
|
||||||
|
'fields' => function() use ($types) {
|
||||||
|
return [
|
||||||
|
'id' => $types->id(),
|
||||||
|
'author' => $types->user(),
|
||||||
|
'body' => [
|
||||||
|
'type' => $types->string(),
|
||||||
|
'args' => [
|
||||||
|
'format' => new EnumType([
|
||||||
|
'name' => 'StoryFormatEnum',
|
||||||
|
'values' => [self::FORMAT_TEXT, self::FORMAT_HTML]
|
||||||
|
]),
|
||||||
|
'maxLength' => $types->int()
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'isLiked' => $types->boolean(),
|
||||||
|
'affordances' => $types->listOf(new EnumType([
|
||||||
|
'name' => 'StoryAffordancesEnum',
|
||||||
|
'values' => [
|
||||||
|
self::EDIT,
|
||||||
|
self::DELETE,
|
||||||
|
self::LIKE,
|
||||||
|
self::UNLIKE
|
||||||
|
]
|
||||||
|
]))
|
||||||
|
];
|
||||||
|
},
|
||||||
|
'interfaces' => [
|
||||||
|
$types->node()
|
||||||
|
],
|
||||||
|
'resolveField' => function($value, $args, $context, ResolveInfo $info) use ($handler) {
|
||||||
|
if (method_exists($handler, $info->fieldName)) {
|
||||||
|
return $handler->{$info->fieldName}($value, $args, $context, $info);
|
||||||
|
} else {
|
||||||
|
return $value->{$info->fieldName};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'containerType' => $handler
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Story $story
|
||||||
|
* @param $args
|
||||||
|
* @param AppContext $context
|
||||||
|
* @return User|null
|
||||||
|
*/
|
||||||
|
public function author(Story $story, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
if ($story->isAnonymous) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return $context->dataSource->findUser($story->authorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Story $story
|
||||||
|
* @param $args
|
||||||
|
* @param AppContext $context
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function affordances(Story $story, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
$isViewer = $context->viewer === $context->dataSource->findUser($story->authorId);
|
||||||
|
$isLiked = $context->dataSource->isLikedBy($story, $context->viewer);
|
||||||
|
|
||||||
|
if ($isViewer) {
|
||||||
|
$affordances[] = self::EDIT;
|
||||||
|
$affordances[] = self::DELETE;
|
||||||
|
}
|
||||||
|
if ($isLiked) {
|
||||||
|
$affordances[] = self::UNLIKE;
|
||||||
|
} else {
|
||||||
|
$affordances[] = self::LIKE;
|
||||||
|
}
|
||||||
|
return $affordances;
|
||||||
|
}
|
||||||
|
}
|
66
examples/01-blog/Blog/Type/UserType.php
Normal file
66
examples/01-blog/Blog/Type/UserType.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog\Type;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\AppContext;
|
||||||
|
use GraphQL\Examples\Blog\Data\User;
|
||||||
|
use GraphQL\Examples\Blog\TypeSystem;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\ResolveInfo;
|
||||||
|
|
||||||
|
class UserType
|
||||||
|
{
|
||||||
|
public static function getDefinition(TypeSystem $types)
|
||||||
|
{
|
||||||
|
$handler = new self();
|
||||||
|
|
||||||
|
return new ObjectType([
|
||||||
|
'name' => 'User',
|
||||||
|
'fields' => function() use ($types) {
|
||||||
|
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(),
|
||||||
|
'error' => [
|
||||||
|
'type' => $types->string(),
|
||||||
|
'resolve' => function() {
|
||||||
|
throw new \Exception("This is error field");
|
||||||
|
}
|
||||||
|
]
|
||||||
|
];
|
||||||
|
},
|
||||||
|
'interfaces' => [
|
||||||
|
$types->node()
|
||||||
|
],
|
||||||
|
'resolveField' => function($value, $args, $context, ResolveInfo $info) use ($handler) {
|
||||||
|
if (method_exists($handler, $info->fieldName)) {
|
||||||
|
return $handler->{$info->fieldName}($value, $args, $context, $info);
|
||||||
|
} else {
|
||||||
|
return $value->{$info->fieldName};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function photo(User $user, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
return $context->dataSource->getUserPhoto($user, $args['size']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lastStoryPosted(User $user, $args, AppContext $context)
|
||||||
|
{
|
||||||
|
return $context->dataSource->findLastStoryFor($user->id);
|
||||||
|
}
|
||||||
|
}
|
165
examples/01-blog/Blog/TypeSystem.php
Normal file
165
examples/01-blog/Blog/TypeSystem.php
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
namespace GraphQL\Examples\Blog;
|
||||||
|
|
||||||
|
use GraphQL\Examples\Blog\Type\Enum\ImageSizeEnumType;
|
||||||
|
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\EnumType;
|
||||||
|
use GraphQL\Type\Definition\ListOfType;
|
||||||
|
use GraphQL\Type\Definition\NonNull;
|
||||||
|
use GraphQL\Type\Definition\ObjectType;
|
||||||
|
use GraphQL\Type\Definition\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class TypeSystem
|
||||||
|
*
|
||||||
|
* Acts as a registry and factory for your types.
|
||||||
|
* As simplistic as possible for the sake of clarity of this example.
|
||||||
|
* Your own may be more dynamic (or even code-generated).
|
||||||
|
*
|
||||||
|
* @package GraphQL\Examples\Blog
|
||||||
|
*/
|
||||||
|
class TypeSystem
|
||||||
|
{
|
||||||
|
// Object types:
|
||||||
|
private $story;
|
||||||
|
private $user;
|
||||||
|
private $image;
|
||||||
|
private $query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ObjectType
|
||||||
|
*/
|
||||||
|
public function story()
|
||||||
|
{
|
||||||
|
return $this->story ?: ($this->story = StoryType::getDefinition($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ObjectType
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->user ?: ($this->user = UserType::getDefinition($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ObjectType
|
||||||
|
*/
|
||||||
|
public function image()
|
||||||
|
{
|
||||||
|
return $this->image ?: ($this->image = ImageType::getDefinition($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ObjectType
|
||||||
|
*/
|
||||||
|
public function query()
|
||||||
|
{
|
||||||
|
return $this->query ?: ($this->query = QueryType::getDefinition($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Interfaces
|
||||||
|
private $nodeDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \GraphQL\Type\Definition\InterfaceType
|
||||||
|
*/
|
||||||
|
public function node()
|
||||||
|
{
|
||||||
|
return $this->nodeDefinition ?: ($this->nodeDefinition = NodeType::getDefinition($this));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Enums
|
||||||
|
private $imageSizeEnum;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return EnumType
|
||||||
|
*/
|
||||||
|
public function imageSizeEnum()
|
||||||
|
{
|
||||||
|
return $this->imageSizeEnum ?: ($this->imageSizeEnum = ImageSizeEnumType::getDefinition());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Scalar types:
|
||||||
|
private $urlType;
|
||||||
|
private $emailType;
|
||||||
|
|
||||||
|
public function email()
|
||||||
|
{
|
||||||
|
return $this->emailType ?: ($this->emailType = EmailType::create());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return UrlType
|
||||||
|
*/
|
||||||
|
public function url()
|
||||||
|
{
|
||||||
|
return $this->urlType ?: ($this->urlType = UrlType::create());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Let's add internal types as well for consistent experience
|
||||||
|
|
||||||
|
public function boolean()
|
||||||
|
{
|
||||||
|
return Type::boolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \GraphQL\Type\Definition\FloatType
|
||||||
|
*/
|
||||||
|
public function float()
|
||||||
|
{
|
||||||
|
return Type::float();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \GraphQL\Type\Definition\IDType
|
||||||
|
*/
|
||||||
|
public function id()
|
||||||
|
{
|
||||||
|
return Type::id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \GraphQL\Type\Definition\IntType
|
||||||
|
*/
|
||||||
|
public function int()
|
||||||
|
{
|
||||||
|
return Type::int();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \GraphQL\Type\Definition\StringType
|
||||||
|
*/
|
||||||
|
public function string()
|
||||||
|
{
|
||||||
|
return Type::string();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Type $type
|
||||||
|
* @return ListOfType
|
||||||
|
*/
|
||||||
|
public function listOf($type)
|
||||||
|
{
|
||||||
|
return new ListOfType($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $type
|
||||||
|
* @return NonNull
|
||||||
|
*/
|
||||||
|
public function nonNull($type)
|
||||||
|
{
|
||||||
|
return new NonNull($type);
|
||||||
|
}
|
||||||
|
}
|
61
examples/01-blog/README.md
Normal file
61
examples/01-blog/README.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
## Blog Example
|
||||||
|
|
||||||
|
Simple but full-featured example of GraphQL API. Models simple blog with Stories and Users.
|
||||||
|
|
||||||
|
Note that graphql-php doesn't dictate you how to structure your application or data layer.
|
||||||
|
You may choose the way of using the library as you prefer.
|
||||||
|
|
||||||
|
Best practices in GraphQL world still emerge, so feel free to post your proposals or own
|
||||||
|
examples as PRs.
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
```
|
||||||
|
php -S localhost:8080 ./index.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Browsing API
|
||||||
|
The most convenient way to browse GraphQL API is by using [GraphiQL](https://github.com/graphql/graphiql)
|
||||||
|
But setting it up from scratch may be inconvenient. The great and easy alternative is to use one of
|
||||||
|
existing Google Chrome extensions:
|
||||||
|
- [ChromeiQL](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij)
|
||||||
|
- [GraphiQL Feen](https://chrome.google.com/webstore/detail/graphiql-feen/mcbfdonlkfpbfdpimkjilhdneikhfklp)
|
||||||
|
|
||||||
|
Note that these extensions may be out of date, but most of the time they Just Work(TM)
|
||||||
|
|
||||||
|
Set up `http://localhost:8080?debug=0` as your GraphQL endpoint/server in these extensions and
|
||||||
|
execute following query:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
viewer {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
}
|
||||||
|
lastStoryPosted {
|
||||||
|
id
|
||||||
|
isLiked
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
photo(size: ICON) {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
type
|
||||||
|
size
|
||||||
|
width
|
||||||
|
height
|
||||||
|
# Uncomment to see error reporting for failed resolvers
|
||||||
|
# error
|
||||||
|
}
|
||||||
|
lastStoryPosted {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
By default this example runs in production mode without additional debugging tools enabled.
|
||||||
|
|
||||||
|
In order to enable debugging mode with additional validation of type definition configs,
|
||||||
|
PHP errors handling and reporting - change your endpoint to `http://localhost:8080?debug=1`
|
83
examples/01-blog/index.php
Normal file
83
examples/01-blog/index.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
// Test this using following command
|
||||||
|
// php -S localhost:8080 ./index.php
|
||||||
|
require_once '../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use \GraphQL\Examples\Blog\TypeSystem;
|
||||||
|
use \GraphQL\Examples\Blog\AppContext;
|
||||||
|
use \GraphQL\Examples\Blog\Data\DataSource;
|
||||||
|
use \GraphQL\Schema;
|
||||||
|
use \GraphQL\GraphQL;
|
||||||
|
use \GraphQL\Type\Definition\Config;
|
||||||
|
|
||||||
|
// Disable default PHP error reporting - we have better one for debug mode (see bellow)
|
||||||
|
ini_set('display_errors', 0);
|
||||||
|
|
||||||
|
if (!empty($_GET['debug'])) {
|
||||||
|
// Enable additional validation of type configs
|
||||||
|
// (disabled by default because it is costly)
|
||||||
|
Config::enableValidation();
|
||||||
|
|
||||||
|
// Catch custom errors (to report them in query results if debugging is enabled)
|
||||||
|
$phpErrors = [];
|
||||||
|
set_error_handler(function($severity, $message, $file, $line) use (&$phpErrors) {
|
||||||
|
$phpErrors[] = new ErrorException($message, 0, $severity, $file, $line);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Initialize user-land registry/factory of our app types:
|
||||||
|
$typeSystem = new TypeSystem();
|
||||||
|
|
||||||
|
// Init stub data source
|
||||||
|
// (in real-world apps this might be Doctrine's EntityManager for instance, or just DB connection):
|
||||||
|
$dataSource = new DataSource();
|
||||||
|
|
||||||
|
// 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->dataSource = $dataSource;
|
||||||
|
$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];
|
||||||
|
|
||||||
|
// GraphQL schema to be passed to query executor:
|
||||||
|
$schema = new Schema([
|
||||||
|
'query' => $typeSystem->query()
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = GraphQL::execute(
|
||||||
|
$schema,
|
||||||
|
$data['query'],
|
||||||
|
null,
|
||||||
|
$appContext,
|
||||||
|
(array) $data['variables']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add reported PHP errors to result (if any)
|
||||||
|
if (!empty($_GET['debug']) && !empty($phpErrors)) {
|
||||||
|
$result['extensions']['phpErrors'] = array_map(
|
||||||
|
['GraphQL\Error\FormattedError', 'createFromPHPError'],
|
||||||
|
$phpErrors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$httpStatus = 200;
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
$httpStatus = 500;
|
||||||
|
if (!empty($_GET['debug'])) {
|
||||||
|
$result['extensions']['exception'] = \GraphQL\Error\FormattedError::createFromException($error);
|
||||||
|
} else {
|
||||||
|
$result['errors'] = \GraphQL\Error\FormattedError::create('Unexpected Error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/json', true, $httpStatus);
|
||||||
|
echo json_encode($result);
|
Loading…
Reference in New Issue
Block a user