query talkGraphQLInPHP {
person(id: "renatomefi") {
name
twitter
company
bioPerLine
}
talk(id: "graphql-php-symfony") {
title
date
type
}
}
{
"data": {
"person": {
"name": "Renato Mendes Figueiredo",
"twitter": "@renatomefi",
"company": "@enrise",
"bioPerLine": [
"ZCE, ZCPE, ZFCE, LPIC-1, LFCS, LFCE Professional",
"a.k.a.: A guy who loves linux and software dev!",
"Co-organizer of @PHPAmersfoort and @rumoazcephp"
]
},
"talk": {
"title": "GraphQL is right in front of us",
"date": "2017-05-17T20:00:00.042Z",
"type": "Meetup talk"
}
}
}
GET /people?limit=10&start=0
GET /person/luke
GET /planet/tatooine
GET /person/luke/starshipConnection?page=1&limit=2
GET /starship/X-wing
GET /starship/ImperialShuttle
“GraphQL prevents HTTP overfetching”
query lukeSkywalker {
person(id: "cGVvcGxlOjE=") {
name
birthYear
height
hairColor
starshipConnection {
starships {
name
}
}
homeworld {
name
}
}
}
{
"data": {
"person": {
"name": "Luke Skywalker",
"birthYear": "19BBY",
"height": 172,
"hairColor": "blond",
"starshipConnection": {
"starships": [
{
"name": "X-wing"
},
{
"name": "Imperial shuttle"
}
]
},
"homeworld": {
"name": "Tatooine"
}
}
}
}
query theGoodMovies {
allFilms(first: 3) {
films {
episodeID
title
}
}
}
{
"data": {
"allFilms": {
"films": [
{
"episodeID": 4,
"title": "A New Hope"
},
{
"episodeID": 5,
"title": "The Empire Strikes Back"
},
{
"episodeID": 6,
"title": "Return of the Jedi"
}
]
}
}
}
query threeAfterR2D2 {
allPeople(first: 3, after: "R2-D2") {
edges {
node {
name
id
}
}
}
}
Almost...
{
"data":{
"allPeople": {
"edges": [
{
"node": {
"name":"Leia Organa",
"id":"cGVvcGxlOjU="
}
}
...
]
}
}
}
Relay Cursor Connections
query twoAfterR2D2 {
allPeople(
first: 2,
after: "YXJyYXljb25uZWN0aW9uOjI="
) {
edges {
node {
name
}
cursor
}
}
}
{
"data": {
"allPeople": {
"edges": [
{
"node": {
"name": "Darth Vader"
},
"cursor": "YXJyYXljb25uZWN0aW9uOjM="
},
{
"node": {
"name": "Leia Organa"
},
"cursor": "YXJyYXljb25uZWN0aW9uOjQ="
}
]
}
}
}
query firstPage {
allPeople (first: 3) {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
name
}
}
}
}
{
"data": {
"allPeople": {
"totalCount": 82,
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjI="
},
"edges": [
... // Nodes
]
}
}
}
query secondPage {
allPeople (
first: 3,
after: "YXJyYXljb25uZWN0aW9uOjI="
) {
totalCount
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
node {
name
}
}
}
}
{
"data": {
"allPeople": {
"totalCount": 82,
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "YXJyYXljb25uZWN0aW9uOjM=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjU="
},
"edges": [
... // Nodes
]
}
}
}
Connections are good for dynamic data
Don't trust it, it's opaque
mutation createReview($episode: String!, $content: String!) {
createReview(episode: $episode, content: $content) {
content
film {
title
}
}
}
Variables can also be used on Queries
mutation createReview(
$episode: String!,
$content: String!
) {
content
film {
title
}
}
{
"episode": "ZmlsbXM6MQ==",
"content": "Be a Jedi!"
}
{
"data": {
"review": {
"content": "...",
"film": {
"title": "A New Hope"
}
}
}
}
{
"errors": [
{
"message": "Variable \"$episode\" of required type \"String!\" was not provided.",
"locations": [
{
"line": 2,
"column": 16
}
]
}
]
}
{
"data": {
"review": null
},
"errors": [
{
"message": "Episode not found",
"locations": [
{
"line": 2,
"column": 30
}
]
}
]
}
$humanType = new ObjectType([
'name' => 'Human',
'description' => 'A humanoid creature in the Star Wars universe.',
'fields' => [
'id' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The id of the human.',
],
'name' => [
'type' => Type::string(),
'description' => 'The name of the human.',
]
],
'resolveType' => function ($obj) use (&$humanType) {
if (isset($humans[$obj['id']])) {
return $humanType;
}
},
]);
$queryString = '
query HeroNameQuery {
hero {
name
}
}
';
$result = GraphQL::execute(
StarWarsSchema::build(),
$queryString
)
# character.types.yml
Character:
type: interface
config:
fields:
id:
type: "String"
name:
type: "String"
# continuation of character.types.yml
...
# Character.config.fields:
friends:
type: "[Character]"
description: "The friends of the character."
appearsIn:
type: "[Film]"
description: "Which movies they appear in."
# human.types.yml
Human:
type: object
config:
fields:
id:
type: "String"
name:
type: "String"
homeWorld:
type: World
interfaces: [Character]
isTypeOf: true
# droid.types.yml
Droid:
type: object
config:
fields:
id:
type: "String"
name:
type: "String"
homeWorld:
type: World
interfaces: [Character]
isTypeOf: true
Interfaces aren't required
# query.types.yml
Query:
type: object
config:
description: "All Star Wars queries"
fields:
allHumans:
type: "[Human]"
resolve: "@=resolver('allHumans', [args])" # Symfony Expression Language
human:
type: "Human"
resolve: "@=resolver('human', [args['id'])" # Symfony Expression Language
args:
id:
description: "Human's UUID"
type: "String!"
# services.yml
services:
graphql.starwars.resolver.human:
class: Mefi/GraphQL/Starwars/Resolver/HumanResolver
arguments:
- "@repository.human"
tags:
- { name: overblog_graphql.resolver, alias: "allHumans", method: "resolveAllHumans" }
- { name: overblog_graphql.resolver, alias: "human", method: "resolveHuman" }
# query.types.yml
# Query.config
fields:
allHumans:
type: "[Human]"
resolve: "@=resolver('allHumans', [args])"
namespace Mefi\GraphQL\Starwars\Resolver;
use Overblog\GraphQLBundle\Definition\Argument;
class HumanResolver
{
// Injected repository, properties and etc ...
public function resolveAllHumans(Argument $args): array
{
// Do something with the $args
return $this->humanRepository->findAll();
}
public function resolveHuman(string $humanId): Human
{
// Your business logic could be here, criterias, firewall, etc
return $this->humanRepository->findById($humanId);
}
}
class HumanResolver
{
// Injected repository, properties and etc ...
// resolveAllHumans ...
public function resolveHuman(string $humanId): Human
{
try {
return $this->humanRepository->findById($humanId);
} catch (HumanNotFoundException $e) {
throw new UserError('This human was eliminated', 0, $e);
}
}
}
throw new UserError('This human was eliminated');
throw new UserWarning('This human is sleeping');
throw new UserErrors(
[
new UserError('Human A not found', 0, $previousException),
'Human B not found',
]
);
/** @var \GraphQL\Validator\Rules\QueryDepth $queryDepth */
$queryDepth = DocumentValidator::getRule('QueryDepth');
$queryDepth->setMaxQueryDepth($maxQueryDepth = 10);
GraphQL::execute(/*...*/);
or with OverBlogGraphQLBundle
# app/config/config.yml
overblog_graphql:
security:
query_max_depth: 10
/** @var \GraphQL\Validator\Rules\QueryComplexity $queryComplexity */
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
$queryComplexity->setMaxQueryComplexity($maxQueryComplexity = 110);
GraphQL::execute(/*...*/);
or with OverBlogGraphQLBundle
# app/config/config.yml
overblog_graphql:
security:
query_max_complexity: 1000
# Query.types.yml
Query:
type: object
config:
fields:
droid:
type: "Droid"
complexity: '@=1000 + childrenComplexity'
args:
id:
description: "id of the droid"
type: "String!"
resolve: "@=resolver('character_droid', [args])"
# human.types.yml
Human:
type: object
config:
fields:
id:
type: "String"
access: "@=isSuperAdmin()"
# human.types.yml
Human:
type: object
config:
fields:
id:
type: "String"
public: "@=service('security.authorization_checker').isGranted('ROLE_ADMIN')"
Human:
type: object
fieldsDefaultPublic: "@=service('my_service').isGranted(typeName, fieldName)"
config:
fields:
id:
type: "String"
namespace Mefi\GraphQL\Starwars\Resolver;
use Overblog\GraphQLBundle\Definition\Argument;
class HumanResolver
{
// Injected repository, properties and etc ...
public function resolveAllHumans(Argument $args): array
{
return $this->humanRepository->findAll();
$criteria = Criteria::create()
$criteria->where(
Criteria::expr()->eq('race', $this->currentUser->getRace())
);
return $this->humanRepository->findAllByCriteria($criteria);
}
}
http://talks.mefi.in/graphql-with-php-010PHP