Leonardo Losoviz
Creator of Gato GraphQL, and contributor to several online magazines, including Smashing Magazine and CSS Tricks.
No under/over fetching of data
Single endpoint
Query mirrors structure of components (React, Vue)
Easy for client-side developers!
No HTTP/Server-side caching
Dependant on tooling (eg: GraphiQL)
Federation?
Difficult for back-end developers!
👍
👎
Declarative way to define the data model
Declarative way to duplicate the data model
Extensible: type, enum, interface, extend, directive, tooling
Extensible? single location, union, directive, tooling, bureaucratic
Versioning, documentation, type validation
Verbose, limited, RESTy, reinvents the wheel
Incremental adoption
Non-comprehensive
author
comments
author
post
/?query=query&variable=value&fragment=fragmentQuery
/?query=field(args)@alias<directive(args)>
/?
query=
field(
args
)@alias<
directive(
args
)
>
/?
query=
posts(
limit: 5
)@posts.
id|
date(format: d/m/Y)|
title<
skip(if: false)
>
/?
query=
posts(
ids: [1, 1499, 1178],
order: $order
)@posts.
id|
date(format: d/m/Y)|
title<
skip(if: false)
>|
--props&
order=title|ASC&
props=
url|
author.
name|
url
/?query=fullSchema
type Human {
name: String
appearsIn: [Episode]
starships: [Starship]
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Starship {
name: String
}
class HumanTypeResolver extends AbstractTypeResolver
{
public function getTypeName(): string
{
return 'Human';
}
public function getSchemaTypeDescription(): ?string
{
return $this->translationAPI->__('Representation of a human');
}
public function getID($resultItem)
{
$human = $resultItem;
return $human->ID;
}
public function getTypeDataLoaderClass(): string
{
return HumanTypeDataLoader::class;
}
}
class HumanTypeDataLoader extends AbstractTypeQueryableDataLoader
{
public function getObjects(array $ids): array
{
$humanTypeAPI = HumanTypeAPIFacade::getInstance();
return $humanTypeAPI->getHumans(['include' => $ids]);
}
public function executeQuery($query, array $options = [])
{
$options['return-type'] = 'ids';
$humanTypeAPI = HumanTypeAPIFacade::getInstance();
return $humanTypeAPI->getHumans($query, $options);
}
}
class HumanFieldResolver extends AbstractDBDataFieldResolver
{
public const NEWHOPE = 'newhope';
public const EMPIRE = 'empire';
public const JEDI = 'jedi';
public static function getClassesToAttachTo(): array
{
return array(
HumanTypeResolver::class,
);
}
public static function getFieldNamesToResolve(): array
{
return [
'name',
'appearsIn',
'starship',
];
}
public function getSchemaFieldType(TypeResolverInterface $typeResolver, string $fieldName): ?string
{
$types = [
'name' => SchemaDefinition::TYPE_STRING,
'appearsIn' => SchemaDefinition::TYPE_ENUM,
'starship' => SchemaDefinition::TYPE_ID,
];
return $types[$fieldName] ?? parent::getSchemaFieldType($typeResolver, $fieldName);
}
public function addSchemaDefinitionForField(array &$schemaDefinition, TypeResolverInterface $typeResolver, string $fieldName): void
{
switch ($fieldName) {
case 'appearsIn':
$schemaDefinition[SchemaDefinition::ARGNAME_ENUMVALUES] = [
self::NEWHOPE,
self::EMPIRE,
self::JEDI,
];
break;
}
}
public function getSchemaFieldDescription(TypeResolverInterface $typeResolver, string $fieldName): ?string
{
$translationAPI = TranslationAPIFacade::getInstance();
$descriptions = [
'name' => $translationAPI->__('Name of the human'),
'appearsIn' => $translationAPI->__('In what film the human appears'),
'starship' => $translationAPI->__('Human\'s starship'),
];
return $descriptions[$fieldName] ?? parent::getSchemaFieldDescription($typeResolver, $fieldName);
}
public function resolveValue(TypeResolverInterface $typeResolver, $resultItem, string $fieldName, array $fieldArgs = [], ?array $variables = null, ?array $expressions = null, array $options = [])
{
// It receives a Human object
$human = $resultItem;
switch ($fieldName) {
case 'name':
return $human->name;
case 'appearsIn':
return $human->episode;
case 'starship':
// It returns a Starship ID, not an object!
return $human->starship;
}
return parent::resolveValue($typeResolver, $resultItem, $fieldName, $fieldArgs, $variables, $expressions, $options);
}
public function resolveFieldTypeResolverClass(TypeResolverInterface $typeResolver, string $fieldName, array $fieldArgs = []): ?string
{
switch ($fieldName) {
case 'starship':
return StarshipTypeResolver::class;
}
return parent::resolveFieldTypeResolverClass($typeResolver, $fieldName, $fieldArgs);
}
}
/?
postId=1&
query=
post($postId).
date(d/m/Y)|
title<
skip(false)
>
/?
postId=1&
query=
post(id:$postId).
date(format:d/m/Y)|
title<
skip(if:false)
>
1. /?query=not(true)
2. /?query=or([1,0])
3. /?query=and([1,0])
4. /?query=if(true, Show this text, Hide this text)
5. /?query=equals(first text, second text)
6. /?query=isNull(),isNull(something)
7. /?query=sprintf(%s API is %s, [PoP, cool])
8. /?query=context
/?query=
post(
id: arrayItem(
posts(
limit: 1,
order: date|DESC
),
0)
)@latestPost.
id|
title|
date
/?
format=Y-m-d&
query=
posts.
if (
hasComments(),
sprintf(
"This post has %s comment(s) and title '%s'", [
commentsCount(),
title()
]
),
sprintf(
"This post was created on %s and has no comments", [
date(format: if(not(empty($format)), $format, d/m/Y))
]
)
)@postDesc
/?query=
posts.
title|
featuredImage<
skip(if:isNull(featuredImage()))
>.
src
/?
format=Y-m-d&
query=
posts.
sprintf(
"This post has %s comment(s) and title '%s'", [
comments-count(),
title()
]
)@postDesc<include(if:has-comments())>|
sprintf(
"This post was created on %s and has no comments", [
date(format: if(not(empty($format)), $format, d/m/Y))
]
)@postDesc<include(if:not(has-comments()))>
/?query=
posts.
title|
featuredImage?.
src
/?query=
echo([
[banana, apple],
[strawberry, grape, melon]
])@fruitJoin<
forEach<
applyFunction(
function: arrayJoin,
addArguments: [
array: %value%,
separator: "---"
]
)
>
>
/?query=
echo([
[
text: Hello my friends,
translateTo: fr
],
[
text: How do you like this software so far?,
translateTo: es
],
])@translated<
forEach<
advancePointerInArray(
path: text,
appendExpressions: [
toLang:extract(%value%,translateTo)
]
)<
translate(
from: en,
to: %toLang%,
oneLanguagePerField: true,
override: true
)
>
>
>
//1. Operators have max-age 1 year
/?query=
echo(Hello world!)
//2. Most fields have max-age 1 hour
/?query=
echo(Hello world!)|
posts.
title
//3. Nested fields also supported
/?query=
echo(posts())
//4. "time" field has max-age 0
/?query=
time
//5. To not cache a response:
//a. Add field "time"
/?query=
time|
echo(Hello world!)|
posts.
title
//b. Add <cacheControl(maxAge:0)>
/?query=
echo(Hello world!)|
posts.
title<cacheControl(maxAge:0)>
//1. Standard behaviour
/?query=
posts.
excerpt
//2. New feature not yet available
/?query=
posts.
excerpt(length:30)
//3. New feature available under
// experimental branch
/?query=
posts.
excerpt(
length:30,
branch:experimental
)
/?query=
me.
name
/?query=
posts.
author.
posts.
comments.
author.
id|
name|
posts.
id|
title|
url|
tags.
id|
slug
// The Google Translate API is called once,
// containing 10 pieces of text to translate:
// 2 fields (title and excerpt) for 5 posts
/?query=
posts(limit:5).
--props|
--props@spanish<
translate(en,es)
>&
props=
title|
excerpt
// Here there are 3 calls to the API, one for
// every language (Spanish, French and German),
// 10 strings each, all calls are concurrent
/?query=
posts(limit:5).
--props|
--props@spanish<
translate(en,es)
>|
--props@french<
translate(en,fr)
>|
--props@german<
translate(en,de)
>&
props=
title|
excerpt
//1. <translate> calls the Google Translate API
/?query=
posts(limit:5).
title|
title@spanish<
translate(en,es)
>
//2. Translate to Spanish and back to English
/?query=
posts(limit:5).
title|
title@translateAndBack<
translate(en,es),
translate(es,en)
>
//3. Change the provider through arguments
// (link gives error: Azure is not implemented)
/?query=
posts(limit:5).
title|
title@spanish<
translate(en,es,provider:azure)
>
/?query=
echo([
usd: [
bitcoin: extract(
getJSON("https://api.cryptonator.com/api/ticker/btc-usd"),
ticker.price
),
ethereum: extract(
getJSON("https://api.cryptonator.com/api/ticker/eth-usd"),
ticker.price
)
],
euro: [
bitcoin: extract(
getJSON("https://api.cryptonator.com/api/ticker/btc-eur"),
ticker.price
),
ethereum: extract(
getJSON("https://api.cryptonator.com/api/ticker/eth-eur"),
ticker.price
)
]
])@cryptoPrices
//1. Get data from a REST endpoint
/?query=
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userEmailLangList
//2. Access and manipulate the data
/?query=
extract(
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"),
email
)@userEmailList
//3. Convert the data into an input to another system
/?query=
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
extract(
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions"),
email
),
"%26emails[]="
)]
)
)@userNameEmailList
/?query=
echo([
github: "https://api.github.com/repos/leoloso/PoP",
weather: "https://api.weather.gov/zones/forecast/MOZ028/forecast",
photos: "https://picsum.photos/v2/list"
])@meshServices|
getAsyncJSON(getSelfProp(%self%, meshServices))@meshServiceData|
echo([
weatherForecast: extract(
getSelfProp(%self%, meshServiceData),
weather.periods
),
photoGalleryURLs: extract(
getSelfProp(%self%, meshServiceData),
photos.url
),
githubMeta: echo([
description: extract(
getSelfProp(%self%, meshServiceData),
github.description
),
starCount: extract(
getSelfProp(%self%, meshServiceData),
github.stargazers_count
)
])
])@contentMesh
// 1. Inspect services
/?query=
meshServices
// 2. Retrieve data
/?query=
meshServiceData
// 3. Process data
/?query=
contentMesh
// 4. Customize data
/?query=
contentMesh(
githubRepo: "getpop/api-graphql",
weatherZone: AKZ017,
photoPage: 3
)@contentMesh
// 1. Save services
/?query=
--meshServices
// 2. Retrieve data
/?query=
--meshServiceData
// 3. Process data
/?query=
--contentMesh
// 4. Customize data
/?
githubRepo=getpop/api-graphql&
weatherZone=AKZ017&
photoPage=3&
query=
--contentMesh
// 1. Access persisted query
/?query=
!contentMesh
// 2. Customize it with variables
/?
githubRepo=getpop/api-graphql&
weatherZone=AKZ017&
photoPage=3&
query=
!contentMesh
Normal schema
Namespaced schema
// Selecting version for fields
/?query=
userServiceURLs(versionConstraint:^0.1)|
userServiceURLs(versionConstraint:">0.1")|
userServiceURLs(versionConstraint:^0.2)
// Selecting version for directives
/?query=
post($postId).
title@titleCase<makeTitle(versionConstraint:^0.1)>|
title@upperCase<makeTitle(versionConstraint:^0.2)>
&postId=1
// Query data for a single resource
{single-post-url}/api/rest/?query=
id|
title|
author.
id|
name
// Query data for a set of resources
{post-list-url}/api/rest/?query=
id|
title|
author.
id|
name
// Output as XML: Replace /graphql with /xml
/api/xml/?query=
posts.
id|
title|
author.
id|
name
// Output as props: Replace /graphql with /props
/api/props/?query=
posts.
id|
title|
excerpt
/api/?query=
posts.
author.
posts.
comments.
author.
id|
name|
posts.
id|
title|
url
//1. Deprecated fields
/?query=
posts.
title|
published
//1. Deprecated fields
/?query=
posts.
title|
published
//2. Schema warning
/?query=
posts(limit:3.5).
title
//3. Database warning
/?query=
users.
posts(limit:name()).
title
//4. Query error
/?query=
posts.
id[book](key:value)
//5. Schema error
/?query=
posts.
non-existant-field|
is-status(
status:non-existant-value
)
//1. Deprecated fields
/?query=
posts.
title|
published
//2. Schema warning
/?query=
posts(limit:3.5).
title
//3. Database warning
/?query=
users.
posts(limit:name()).
title
/?query=
posts(limit:3.5).
title
/?query=
post(divide(a,4)).
title
/?query=
echo([hola,chau])<
forEach<
translate(notexisting:prop)
>
>
/?
actions[]=show-logs&
postId=1&
query=
post($postId).
title|
date(d/m/Y)
/?
postId=1&
query=
post($postId)@post.
content|
date(d/m/Y)@date,
getJSON("https://newapi.getpop.org/wp-json/newsletter/v1/subscriptions")@userList|
arrayUnique(
extract(
getSelfProp(%self%, userList),
lang
)
)@userLangs|
extract(
getSelfProp(%self%, userList),
email
)@userEmails|
arrayFill(
getJSON(
sprintf(
"https://newapi.getpop.org/users/api/rest/?query=name|email%26emails[]=%s",
[arrayJoin(
getSelfProp(%self%, userEmails),
"%26emails[]="
)]
)
),
getSelfProp(%self%, userList),
email
)@userData,
self.
post($postId)@post<
copyRelationalResults(
[content, date],
[postContent, postDate]
)
>|
self.
getSelfProp(%self%, postContent)@postContent<
translate(
from: en,
to: arrayDiff([
getSelfProp(%self%, userLangs),
[en]
])
),
renameProperty(postContent-en)
>|
getSelfProp(%self%, userData)@userPostData<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: postContent,
array: %value%,
value: getSelfProp(
%self%,
sprintf(
postContent-%s,
[extract(%value%, lang)]
)
)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: ""
),
addArguments: [
key: header,
array: %value%,
value: sprintf(
string: "<p>Hi %s, we published this post on %s, enjoy!</p>",
values: [
extract(%value%, name),
getSelfProp(%self%, postDate)
]
)
]
)
>
>|
self.
getSelfProp(%self%, userPostData)@translatedUserPostProps<
forEach(
if: not(
equals(
extract(%value%, lang),
en
)
)
)<
advancePointerInArray(
path: header,
appendExpressions: [
toLang: extract(%value%, lang)
]
)<
translate(
from: en,
to: %toLang%,
oneLanguagePerField: true,
override: true
)
>
>
>|
self.
getSelfProp(%self%,translatedUserPostProps)@emails<
forEach<
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: content,
array: %value%,
value: concat([
extract(%value%, header),
extract(%value%, postContent)
])
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: to,
array: %value%,
value: extract(%value%, email)
]
),
applyFunction(
function: arrayAddItem(
array: [],
value: []
),
addArguments: [
key: subject,
array: %value%,
value: "PoP API example :)"
]
),
sendByEmail
>
>
/?query=
addPost($title, $content).
addComment($comment1)|
addComment($comment2).
author<sendConfirmationByEmail>.
followers<notifyByEmail, notifyBySlack>
"require": {
"getpop/graphql": "dev-master",
"getpop/engine-wp": "dev-master",
"getpop/posts-wp": "dev-master",
"getpop/users-wp": "dev-master",
"getpop/comments-wp": "dev-master",
"getpop/pages-wp": "dev-master",
"getpop/postmedia-wp": "dev-master",
"getpop/taxonomies-wp": "dev-master"
}
By Leonardo Losoviz
“GraphQL by PoP” is a brand-new GraphQL server in PHP, CMS-agnostic (adapters currently implemented for WordPress), which implements the GraphQL spec and expands it with several additional features
Creator of Gato GraphQL, and contributor to several online magazines, including Smashing Magazine and CSS Tricks.