First official example that should help newcomers to start (incomplete yet, but still useful)

This commit is contained in:
vladar 2016-10-21 18:43:11 +07:00
parent 6c076e21d4
commit d41687913a
17 changed files with 991 additions and 1 deletions

View File

@ -25,7 +25,8 @@
"autoload-dev": {
"psr-4": {
"GraphQL\\Tests\\": "tests/",
"GraphQL\\Benchmarks\\": "benchmarks/"
"GraphQL\\Benchmarks\\": "benchmarks/",
"GraphQL\\Examples\\Blog\\": "examples/01-blog/Blog/"
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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`

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