query workshopGraphQLSF {
person(id: "renatomefi") {
name
twitter
company
bioPerLine
}
talk(id: "graphql-php-symfony") {
title
date
type
}
}
{
"data": {
"person": {
"name": "Renato Mendes Figueiredo",
"twitter": "@renatomefi",
"company": "@usabilla",
"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",
"Maintainer of php-vcr and OverblogGraphQLBundle"
]
},
"talk": {
"title": "Workshop GraphQL in Symfony",
"date": "2018-06-07T13:45:00.042Z",
"type": "DPC 2018"
}
}
}
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"
}
]
}
}
}
composer create-project "symfony/skeleton:^4.1" graphql-workshop
composer require server --dev
./app/console server:run
php -dxdebug.remote_enable=1 -dxdebug.remote_mode=req -dxdebug.remote_port=9000 -dxdebug.remote_host=127.0.0.1 -S 127.0.0.1:8080 ./public/index.php
composer req "overblog/graphql-bundle:0.11"
# config/graphql/types/Query.yaml
Query:
type: object
config:
description: "Main Queries"
fields:
hello:
type: "String"
resolve: "World"
composer req "overblog/graphiql-bundle" --dev
open http://127.0.0.1:8000/graphiql
query {
hello
}
# config/graphql/types/Query.yaml
Query:
type: object
config:
description: "Main Queries"
fields:
hello:
type: "String"
resolve: "World"
helloFromInput:
type: "String"
args:
name:
type: "String"
resolve: "@=args['name']"
resolve: "@=args['name'] ?: 'Nothing'"
resolve: "@=args['name'] ?: 'Guest'"
# config/graphql/types/Mutation.yaml
Mutation:
type: object
config:
description: "Main Mutations"
fields:
sayHello:
type: "String"
args:
hello:
type: "String!"
resolve: "@='your input is: ' ~ args['hello']"
# config/graphql/types/Mutation.yaml
...
fields:
createWorkshop:
type: "String"
args:
workshop: "WorkshopInput!"
resolve: "@=args['workshop']['conference']~':'~args['workshop']['name']"
...
WorkshopInput:
type: input-object
config:
fields:
conference:
type: "String!"
name:
type: "String!"
# src/Domain/Workshop.php
namespace App\Domain;
final class Workshop
{
/**
* @var string
*/
private $conference;
/**
* @var string
*/
private $name;
public function __construct(string $name, string $conference)
{
$this->name = $name;
$this->conference = $conference;
}
public function getConference(): string
{
return $this->conference;
}
public function getName(): string
{
return $this->name;
}
}
# config/graphql/types/Mutation.yaml
WorkshopInput:
...
Workshop:
type: object
config:
fields:
conference:
type: "String!"
name:
type: "String!"
# src/Infrastructure/GraphQL/Workshop/Mutation/Create.php
namespace App\Infrastructure\GraphQL\Workshop\Mutation;
use App\Domain\Workshop;
final class Create
{
public function __invoke(array $arguments): Workshop
{
return new Workshop(
$arguments['name'],
$arguments['conference']
);
}
}
# config/graphql/types/Mutation.yaml
...
createWorkshop:
type: "Workshop"
args:
workshop: "WorkshopInput!"
resolve: '@=mutation("App\\Infrastructure\\GraphQL\\Workshop\\Mutation\\Create", [args["workshop"]])'
mutation {
createWorkshop(workshop: {conference: "DPC", name: "GraphQL"}) {
conference
name
}
}
{
"errors": [
{
"debugMessage": "Unknown mutation with alias \"App\\Infrastructure\\GraphQL\\Workshop\\Mutation\\Create\" (verified service tag)",
"message": "Internal server Error",
"category": "internal",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"createWorkshop"
]
}
# config/services.yaml
...
services:
...
App\Infrastructure\GraphQL\Workshop\Mutation\Create:
tags: ['overblog_graphql.mutation']
# config/services.yaml
...
services:
...
_instanceof:
Overblog\GraphQLBundle\Definition\Resolver\MutationInterface:
tags: ['overblog_graphql.mutation']
Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface:
tags: ['overblog_graphql.resolver']
GraphQL\Type\Definition\Type:
tags: ['overblog_graphql.type']
# src/Infrastructure/GraphQL/Workshop/Mutation/Create.php
namespace App\Infrastructure\GraphQL\Workshop\Mutation;
use App\Domain\Workshop;
use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface;
final class Create implements MutationInterface
...
# config/graphql/types/Domain/Workshop.yaml
Workshop:
type: object
config:
fields:
conference:
type: "String!"
name:
type: "String!"
WorkshopInput:
type: input-object
config:
fields:
conference:
type: "String!"
name:
type: "String!"
# config/graphql/types/Domain/Workshop.yaml
WorkshopPrototype:
decorator: true
config:
fields:
conference:
type: "String!"
name:
type: "String!"
Workshop:
type: object
inherits: [WorkshopPrototype]
config:
fields:
id:
type: "String!"
WorkshopInput:
type: input-object
inherits: [WorkshopPrototype]
composer req ramsey/uuid
Implement getId() on Workshop
# src/Infrastructure/GraphQL/UuidType.php
namespace App\Infrastructure\GraphQL;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\ScalarType;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Ramsey\Uuid\Uuid;
final class UuidType extends ScalarType implements AliasedInterface
{
private const NAME = 'UUID';
public function __construct()
{
parent::__construct([
'name' => self::NAME,
'description' => 'A UUID represented as string'
]);
}
public function serialize($value): ?string
{
return \is_string($value) && Uuid::isValid($value) ? $value : null;
}
public function parseValue($value): ?string
{
return \is_string($value) && Uuid::isValid($value) ? $value : null;
}
public function parseLiteral($valueAST): ?string
{
if (!$valueAST instanceof StringValueNode) {
return null;
}
return $this->parseValue($valueAST->value);
}
public static function getAliases(): array
{
return [self::NAME];
}
}
# config/graphql/types/Domain/Workshop.yaml
Workshop:
type: object
inherits: [WorkshopPrototype]
config:
fields:
id:
type: "UUID!"
# src\App\Infrastructure\GraphQL\Workshop\Resolver\Workshop
namespace App\Infrastructure\GraphQL\Workshop\Resolver;
use App\Domain\Workshop as WorkshopVo;
use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface;
use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface;
final class Workshop implements ResolverInterface, AliasedInterface
{
public function allWorkshops(): array
{
return [
new WorkshopVo('GraphQL with Symfony', 'DPC 2018', '5cf8c6ac-7f40-46a1-b666-2c262d4e8abe'),
new WorkshopVo('GraphQL with Symfony', 'FWDays 2018', 'aab5088d-6b59-4e00-84b8-3e71943fd2a1'),
];
}
public function workshopById(string $id): ?WorkshopVo
{
return array_filter($this->allWorkshops(), function (WorkshopVo $workshop) use ($id) {
return $workshop->getId() === $id;
})[0] ?? null;
}
public static function getAliases(): array
{
return [
'allWorkshops' => 'app.graphql.resolver.workshop.all',
'workshopById' => 'app.graphql.resolver.workshop.by.id',
];
}
}
# config/graphql/types/Query.yaml
Query:
inherits:
- WorkshopQuery
...
WorkshopQuery:
decorator: true
config:
fields:
WorkshopsDirectly:
type: "[Workshop]"
resolve: '@=resolver("App\\Infrastructure\\GraphQL\\Workshop\\Resolver\\Workshop::allWorkshops")'
# config/graphql/types/Query.yaml
...
WorkshopQuery:
decorator: true
config:
fields:
Workshops:
type: "[Workshop]"
resolve: '@=resolver("app.graphql.resolver.workshop.all")'
Workshop:
type: "Workshop"
args:
id:
type: "UUID!"
resolve: '@=resolver("app.graphql.resolver.workshop.by.id", [args["id"]])'
# config/graphql/types/Query.yaml
...
WorkshopQuery:
decorator: true
config:
fields:
WorkshopsRelay:
type: "WorkshopConnection"
argsBuilder: "Relay::Connection"
resolve: '@=resolver("app.graphql.resolver.workshop.relay", [args])'
# src\App\Infrastructure\GraphQL\Workshop\Resolver\Workshop
...
public function relayWorkshops($args): Connection
{
$data = $this->allWorkshops();
return ConnectionBuilder::connectionFromArraySlice(
$data,
$args,
[
'arrayLength' => \count($data),
]
);
}
...
public static function getAliases(): array
{
return [
...
'relayWorkshops' => 'app.graphql.resolver.workshop.relay',
];
}
# config/graphql/types/Domain/Workshop.yaml
Workshop:
type: object
inherits: [WorkshopPrototype]
config:
fields:
id:
type: "UUID!"
enrolledPeople: <================
type: "[Person]"
description: "All People related to this Workshop"
resolve: '@=resolver("app.graphql.resolver.people.by.workshop", [value, args])'
# App\Infrastructure\GraphQL\Workshop\Resolver\WorkshopPeople
...
public const ENROLLMENT_TYPE_TUTOR = 'tutor';
public const ENROLLMENT_TYPE_ATTENDEE = 'attendee';
private static $data = [
'5cf8c6ac-7f40-46a1-b666-2c262d4e8abe' => [
'3317742c-1dec-43d1-b1eb-06634a58e95b' => self::ENROLLMENT_TYPE_TUTOR,
'd186ebf4-9de1-4eb6-b5ab-fbc07f7ca1d6' => self::ENROLLMENT_TYPE_ATTENDEE,
],
'aab5088d-6b59-4e00-84b8-3e71943fd2a1' => [
'9099d144-fc0b-417c-a003-eb9396a3e264' => self::ENROLLMENT_TYPE_TUTOR,
'3317742c-1dec-43d1-b1eb-06634a58e95b' => self::ENROLLMENT_TYPE_ATTENDEE,
]
];
...
# App\Infrastructure\GraphQL\Workshop\Resolver\WorkshopPeople
...
/**
* @return Person[]
*/
public function resolvePeopleByWorkshop(Workshop $workshop, Argument $args): Generator
{
$peopleIds = self::$data[$workshop->getId()];
foreach ($peopleIds as $personId => $role) {
yield $this->personRepository->findById($personId);
}
}
...
# config/graphql/types/Domain/Person.yaml
Person:
type: object
config:
fields:
enrolledWorkshops:
type: "[Workshop]"
resolve: "@=resolver('app.graphql.resolver.workshop.by.people', [value])"
# App\Infrastructure\GraphQL\Workshop\Resolver\WorkshopPeople
...
public function resolveWorkshopsByPerson(Person $person): Generator
{
foreach (self::$data as $workshopId => $people) {
if (array_key_exists($person->getId(), $people)) {
yield $this->workshopRepository->findById($workshopId);
}
}
}
...
# config/graphql/types/Domain/Workshop.yaml
Workshop:
...
enrolledPeople:
type: "[Person]"
args:
enrollmentType:
type: WorkshopEnrollment
...
WorkshopEnrollment:
type: enum
config:
description: "Possible enrollment types between Workshop and Person"
values:
TUTOR:
value: '@=constant("App\\Infrastructure\\GraphQL\\Workshop\\Resolver\\WorkshopPeople::ENROLLMENT_TYPE_TUTOR")'
ATTENDEE:
value: '@=constant("App\\Infrastructure\\GraphQL\\Workshop\\Resolver\\WorkshopPeople::ENROLLMENT_TYPE_ATTENDEE")'
# config/packages/graphql.yaml
overblog_graphql:
security:
query_max_depth: 3
# config/packages/graphql.yaml
overblog_graphql:
security:
query_max_depth: 3
query_max_complexity: 48
# config/graphql/types/Domain/Person.yaml
...
enrolledWorkshops:
...
complexity: '@=2 + childrenComplexity'
# config/graphql/types/Domain/Person.yaml
Person:
config:
fields:
...
bornDate:
type: "Date!"
access: '@=service("App\\Infrastructure\\GraphQL\\Workshop\\Resolver\\WorkshopPeople").canAccessBornDate(value)'
# App\Infrastructure\GraphQL\Workshop\Resolver\WorkshopPeople
...
public function canAccessBornDate(Person $person): bool
{
return $person->getId() !== '3317742c-1dec-43d1-b1eb-06634a58e95b';
}
...
# config/graphql/types/Domain/Person.yaml
Person:
config:
fields:
...
bornDate:
type: "Date!"
public: '@=service("App\\Infrastructure\\GraphQL\\Workshop\\Resolver\\WorkshopPeople").canViewBornDate(typeName, fieldName)'
access: '@=service("App\\Infrastructure\\GraphQL\\Workshop\\Resolver\\WorkshopPeople").canAccessBornDate(value)'
# App\Infrastructure\GraphQL\Workshop\Resolver\WorkshopPeople
...
public function canViewBornDate(string $typeName, string $fieldName): bool
{
return true;
}
...