Tutorial: Building a GraphQL API in PHP

Andrew Rota
Andrew RotaSoftware Architect at Wayfair en Wayfair
Building a GraphQL API in PHP
@AndrewRota | Longhorn PHP 2019
Code: bit.ly/graphql-php-tutorial-repo
Glitch: bit.ly/graphql-php-glitch
Complete App: bit.ly/complete-graphql-php
Complete Code: bit.ly/complete-graphql-php-glitch
Today we’ll learn about GraphQL and
build a GraphQL API in PHP which can
be consumed with a JavaScript
frontend.
By the end of the workshop you will:
- Understand what GraphQL is and how it works
- Have built a GraphQL API in PHP
- Know why GraphQL might be the right choice for your
application
Andrew Rota
@AndrewRota
Associate Director, Software Engineering
Challenges traditional APIs
‣ Over-fetching data
‣ Under-fetching data, requiring multiple round-trips
‣ Time spent iterating on endpoints and expected data shape
GraphQL offers an alternative
architecture for developing efficient,
easy to understand APIs.
What is GraphQL?
“GraphQL is a query
language for APIs and a
runtime for fulfilling those
queries with your existing
data.”
{
conferences {
name
dates
}
}
"conferences": [
{
"name": "Longhorn PHP",
"dates": "May 2-4, 2019"
}
]
{
conferences {
name
speakers {
name
twitter
}
}
}
{
"conferences": [
{
"name": "Longhorn PHP",
"speakers": [
{
"name": "Andrew Rota",
"twitter": "https://twitter.com/AndrewRota"
}
]
}
]
}
Tutorial
Agenda
❏ Intro to GraphQL Concepts
❏ Setup our PHP project
❏ Queries
❏ Types and Resolvers
❏ Deferred Resolvers
❏ Mutations
❏ GraphQL on the Frontend
❏ Q&A
What we’re Building
An API for querying a database of PHP conferences and speakers.
What we’re Building
An API for querying a database of PHP conferences and speakers.
What we’re Building
An API for querying a database of PHP conferences and speakers.
phpconferences.sqlite `conferences` table:
`talks` table:
`speakers` table:
Development Environment
I recommend “Glitch,” a browser-based editor with a server environment that runs PHP code.
bit.ly/graphql-php-glitch
Development Environment
You can also clone the repo and run it locally with PHP 7.2 w/ SQLite and Composer.
Run: `composer install; composer run start --timeout=0`
Introduction to GraphQL
GraphQL
‣ Developed internally at Facebook in 2012
‣ Open sourced in 2015
‣ Spec: facebook.github.io/graphql/draft
GraphQL Implementations
‣ GraphQL is technology agnostic for both client and server
‣ Client implementations:
‣ Server implementations:
GraphQL Advantages
‣ Client requests exactly the shape of the data it needs
‣ Multiple resources can be queried in a single request
‣ API is defined with a strongly-typed schema
‣ Supports powerful tooling for developers
‣ Streamlines frontend ← → backend API conversations
GraphQL in a web stack
QueryClient
(e.g., browser, mobile
app)
/graphql on
PHP Server
response
Database
GraphQL Concepts
Queries + Fields
‣ In GraphQL you make queries
for fields on objects
‣ The response will have the
same shape as the query
query {
conferences {
name
dates
}
}
query
field
Fields
‣ Fields might be scalar values,
or they might be other Objects.
‣ Fields can refer to Objects, and
you can make a sub-selection
for fields of these Objects.
‣ This lets you avoid making
multiple requests for related
resources
query {
conferences {
name
speakers {
name
}
}
}
sub-selection
Arguments
‣ You can pass named
arguments to each field
and nested object.
{
conference(name: "Longhorn PHP") {
speakers {
name
}
}
}
argument
Variables
‣ Dynamic values can be
passed into queries via
variables
query SearchConfs($name: String){
conferences(nameFilter:$name) {
name
}
}
{"name": "Longhorn"}
EXERCISE #1
Write and run GraphQL queries
1. Go to
https://graphql.github.io/swapi-g
raphql
2. Run a query (use docs tab to
explore graph). For example:
Types + Schemas
‣ Every GraphQL service
defines the a set of types
that describe what data can
be requested
Types + Schemas
‣ GraphQL servers can be
written in any language, so
we describe types with a
language-agnostic “GraphQL
schema language”
type Conference {
name: String!
url: String!
description: String
location: String
dates: String!
# List of speakers at this conference
speakers: [Speaker]
}
Types + Schemas
‣ GraphQL servers can be
written in any language, so
we describe types with a
language-agnostic “GraphQL
schema language”
‣ Types include: object, scalar,
list, enumeration, union,
interface, and non-nullable.
type Conference {
name: String!
url: String!
description: String
location: String
dates: String!
# List of speakers at this conference
speakers: [Speaker]
}
non-nullable
scalar type
list of
object
types
Query + Mutation Types
‣ There are two special types
in every GraphQL schema:
Query and Mutation
‣ Root fields you define on
Query and Mutation are the
entry points of requests
type Query {
# Returns conferences
conferences: [Conference]
# Returns speakers
speakers: [Speaker]
}
root
fields
root type
Queries
‣ Queries ask for for data;
analogous to GET requests.
‣ GraphQL clients (e.g.,
browsers, mobile apps),
make queries against a single
GraphQL endpoint
‣ Operation name and type
can be optional
query ConferenceNamesAndDates{
conferences {
name
dates
}
}
operation nameoperation type
fields
Mutations
‣ Mutations are for modifying
data; analogous to
POST/PUT/DELETE requests.
‣ They start with the mutation
root type, and will often
leverage arguments, but are
otherwise the same as
queries
mutation {
addSpeaker(
name: "Andrew Rota",
twitter: "https://twitter.com/andrewrota")
{
id
}
}
GraphQL Tooling
Introspection
‣ A key feature of GraphQL is
its introspection system
‣ You can ask any GraphQL
schema about what queries
it supports
‣ This unlocks opportunities
for powerful tooling
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
},
"types": [
{
"kind": "OBJECT",
"name": "Query",
"description": null,
"fields": [
{
"name": "conferences",
"description": "Returns a list of PHP conferences",
"args": [],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Conference",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
]
}
]
}
}
}
GraphiQL - in browser IDE for exploring GraphQL
Voyager - Any GraphQL API as an interactive graph
Graphql Playground - like GraphiQL, but more features
PHPStorm JS GraphQL Plugin - IDE Integration
EXERCISE #2
-- Discussion --
Explore an API with Voyager
● What did you notice about the
data graph?
● How does the data compare or
contrast with data in applications
you work with?
EXERCISE #2
Explore an API with Voyager
1. Go to apis.guru/graphql-voyager
2. Select and API and explore!
GraphQL on the Server
EXERCISE #3
Setup our PHP workspace with
Glitch
1. Go to: bit.ly/graphql-php-glitch
2. Click “Remix to Edit”
3. Explore and run the project
● index.php: links to voyager and
graphql-playground and JS
clients
● graphql.php: the graphql
endpoint
EXERCISE #3
Modify the code and see it run in
graphql_playground.php
1. In graphql.php, add “hello world”
string to the output array
2. Test that the endpoint now returns
that array in graphql.php
The GraphQL Server Library
webonyx/graphql-php
Provides:
‣ Type primitives for your Type system
‣ Query parsing, validation, and
execution against a Type system
‣ Type introspection
‣ Tools for deferred field resolution
Feature-complete implementation of the
GraphQL spec in PHP, inspired by
Facebook’s original node-js reference library.
webonyx/graphql-php
Used by:
‣ Folkloreatelier/laravel-graphql
‣ overblog/GraphQLBundle (symfony)
‣ ivome/graphql-relay-php
‣ wp-graphql/wp-graphql
‣ tim-field/graphql-wp
Feature-complete implementation of the
GraphQL spec in PHP, inspired by
Facebook’s original node-js reference library.
EXERCISE #3: Install graphql-php
1. In the console, run:
composer require webonyx/graphql-php; refresh;
2. Check composer.json for the webonyx/graphql-php entry
Schemas start with the special root type, Query.
Creating a Query Type
‣ ObjectType is a collection of
fields
‣ Query has root fields, the
entry points for queries.
‣ Each field must have a name
and type. It can also have a
resolve function, args,
description, and other
properties.
use GraphQLTypeDefinitionObjectType;
use GraphQLTypeDefinitionType;
$queryType = new ObjectType([
'name' => 'Query',
'fields' => [
'message' => [
'type' => Type::string(),
'resolve' => function ($root, $args) {
return 'hello world';
}
],
]
]);
Exercise #4
Creating the Query type
1. See the Query type
configuration in
App/Type/QueryType.php
2. Add a field “message” with a
resolver that returns a string
$config = [
'name' => 'Query',
'fields' => [
'message' => [
'type' => Type::string(),
'resolve' => function () {
return 'hello world!';
}
],
]
];
The graphql.php endpoint should take a query, execute it against
a schema, and return the data from resolvers.
1. Parse the query into an AST
2. Validate the query against the schema
3. Traverse the query, breadth first, and execute a resolver
function for each field
Facade Method for
Query Execution
‣ graphql-php offers a facade
for this process in the class
GraphQLGraphQL
use GraphQLGraphQL;
$result = GraphQL::executeQuery(
$schema,
$queryString,
$rootValue = null,
$context = null,
$variableValues = null,
$operationName = null,
$fieldResolver = null,
$validationRules = null
);
$serializableResult = $result->toArray();
Exercise #5
Initialize Schema and Execute Query
1. Initialize Schema instance with
Query type
2. Execute query and return result
// graphql.php
$schema = new Schema([
'query' => Types::query()
]);
$result = GraphQL::executeQuery(
$schema,
$data['query'],
null,
null,
(array)$data['variables']
);
$output = $result->toArray($debug);
Exercise #6
Add to the message field
1. Add an argument to the
message field
2. Make the argument required
(hint: use Type::nonNull)
3. Add a description to the field,
and view it in docs tab of
graphql_playground
$config = [
'name' => 'Query',
'fields' => [
'message' => [
'type' => Type::string(),
'args' => [
'name' => Type::string(),
],
'resolve' => function ($root, $args) {
return 'hello' . $args['name'];
}
],
]
];
Building a GraphQL server is primarily about structuring schema
types, and then implementing their field resolvers
The schema defines what queries can be made, what types of
data can be requested, and the relationships between those types
The resolver functions define how to get the data for each field.
definition
implementation
Types
graphql-php provides types, which are instances of classes from GraphQLTypeDefinition
○ ObjectType → collection of fields, each with their own type
○ ScalarType → built in scalars types: string, int, float, boolean, or id
○ InterfaceType → abstract type with a collection of fields
○ UnionType → abstract type which enumerates other possible object types
○ InputObjectType → like ObjectType, but for complex args inputted by user
○ EnumType → scalar type which is restricted to a set of allowed values
○ listOf → array (or Traversable) list of another type
○ nonNull → any type which is guaranteed to not be null
Types
graphql-php provides types, which are instances of classes from GraphQLTypeDefinition
○ ObjectType → collection of fields, each with their own type
○ ScalarType → built in scalars types: string, int, float, boolean, or id
○ InterfaceType → abstract type with a collection of fields
○ UnionType → abstract type which enumerates other possible object types
○ InputObjectType → like ObjectType, but for complex args inputted by user
○ EnumType → scalar type which is restricted to a set of allowed values
○ listOf → array (or Traversable) list of another type
○ nonNull → any type which is guaranteed to not be null
Common pattern for adding new objects to the graph:
1. Define a custom object type
2. Add it to a new field on an existing object type
3. Write the field resolver function
Custom Object Types
○ We can define custom types by extending the ObjectType
○ These types will be a collection of fields, which could be other
custom or builtin types
Type Registry
Each type must only be a single instance
in the Schema, so we can use a registry
class with static functions to store each
type.
Our type registry is in App/Types.php
Whenever we add a new type, we should
also add it to the type registry.
/**
* @return QueryType
*/
public static function query()
{
return self::$query ?: (self::$query = new
QueryType());
}
Let’s create our first custom type
Exercise #7
Add a conference type
1. Define the type for a conference in
App/Type/ConferenceType.php
2. Add all of the type’s scalar fields
(i.e., omit the more complex
“speaker” type)
3. Add the type to the types registry
(App/Types.php)
$config = [
'name' => 'Conference',
'fields' => [
'id' => Type::nonNull(Types::int()),
'name' => Type::nonNull(Types::string()),
'url' => Type::nonNull(Types::string()),
'description' => Types::string(),
'location' => Types::string(),
'dates' => Type::nonNull(Types::string())
]
];
phpconferences.sqlite `conferences` table:
`talks` table:
`speakers` table:
Exercise #8a
Add conferences to root query
1. Add conferences field to the
root query that returns all the
conferences with
DataSource::getConferences
2. Add an argument to filter by
name, using
DataSource::searchConferencesByName
'conferences' => [
'type' => Types::listOf(Types::conference()),
'description' => 'List PHP Conferences',
'resolve' => function() {
return DataSource::getConferences();
}
],
Exercise #8b
Add getConferenceById field
1. Create another root field for
getting conferences, but with
an argument to select
conference by id using
DataSource::getConferenceById
'conferences' => [
'type' => Types::listOf(Types::conference()),
'description' => 'List PHP Conferences',
'resolve' => function() {
return DataSource::getConferences();
}
],
Data fetching happens in resolve functions.
Resolvers
‣ Resolve function can be
implemented however you’d like to
get the data: SQL queries, cache, or
another API.
‣ Start by implementing the resolver
on the Query’s field
‣ For scalars, the return value will be
the value of the field
‣ For object types, the return value
will be passed on to nested fields
// QueryType.php
'conferences' => [
'type' => Types::listOf(Types::Conference()),
'description' => 'List PHP Conferences',
'resolve' => function() {
return DataSource::getConferences();
}
],
Default + Custom Resolvers
‣ The default field resolver for
objects will return the value
(or null) by key or property
on the object
‣ Or you can implement a
custom resolver at the type
or field level itself
○ ‘resolveField’ on type
○ ‘resolve’ on field
// SomeType.php
'fields' => [
// Default resolvers return property on object
'id' => Type::nonNull(Types::int()),
'name' => Type::nonNull(Types::string()),
// Custom resolver function
'otherField' => ['type' => Types::string(), 'resolve' =>
function ($value) {
return 'some other value';
}],
// Field not on object, to be handled by resolveField
'formattedName' => Type::nonNull(Types::string()),
],
// Custom resolveField function for type
'resolveField' => function($value, $args, $ctx, $info) {
if ($info->fieldName === 'formattedName') {
return $value->name . '!';
}
return $value->{$info->fieldName};
}
...let’s take a closer look at a resolve function
function($root, $args, $context, ResolveInfo $info) {
return DataSource::getData($root->id);
}
root / parent
result
arguments app context
query AST and
other meta info
Exercise #9
Add custom field resolvers
1. Add a custom field to the
conference type that returns a
formatted value not available on
the original object.
2. Refactor to use the field-level
resolve function and type-level
resolveField function
'formattedName' => [
'type' => Types::string(),
'resolve' => function($value) {
return $value->name . '!!! 🎉';
}
]
Let’s add another type and root field!
Exercise #10
Add speakers type and fields
1. Add a Speakers type in App/Type/SpeakerType.php
2. Add a root `speakers` field in QueryType.php to get all speakers with
DataSource::getSpeakers()
3. Add a speakers field to conference, and resolve data with
DataSource::getSpeakersAtConference($id)
Context
‣ A value to hold information
shared by all resolvers, e.g.,
user data, request metadata,
etc.
‣ Set initially as 4th argument
to GraphQL::executeQuery()
‣ Available as the 3rd
argument in all resolvers
// graphql.php
$result = GraphQL::executeQuery(
$schema,
$data['query'],
null,
$appContext,
(array) $data['variables']
);
// resolver function
function($value, $args, $context) {
return $context->someValue;
}
Exercise #11
Add context
1. Add a context object in
executeQuery with some global
data (e.g., a hard coded username)
2. Output that context as part of a field
resolver’s return value.
// graphql.php
class AppContext{
public $user;
}
// ...
$appContext = new AppContext();
$appContext->user = 'something';
// ...
$result = GraphQL::executeQuery(
$schema,
$data['query'],
null,
$appContext,
(array) $data['variables']
);
Performance
n+1 problem
‣ Data-fetching problem that
occurs when you need to
fetch related items in a
one-to-many relationship
{
conferences {
name
speakers{
name
}
}
}
Solution: deferred resolvers
‣ We can use GraphQLDeferred to
delay field resolution until we can
make a single batch request for the
data
‣ Once all non-deferred fields are
resolved, graphql-php will call the
wrapped closures
‣ If you have an environment that
supports async operations (e.g.,
HHVM, ReactPHP, PHP threads),
some fields can also resolve async.
'resolve' => function($root) {
SpeakerCollection::add($root->id);
return new Deferred(function() use ($root) {
return SpeakerCollection::get($root->id);
});
}
Exercise #12
Implement a deferred resolver
1. Refactor speakers field on
Conference type to use a deferred
resolver
2. See App/SpeakerCollection.php
and DataSource::selectSpeakers to
see how this might be more
efficient
'resolve' => function($root) {
SpeakerCollection::add($root->id);
return new Deferred(function() use ($root) {
return SpeakerCollection::get($root->id);
});
}
Mutations
So far we’ve been reading data with queries, but like any API,
GraphQL can also manipulate data with mutations.
Mutation Types
‣ Mutation is a special root type, just
like Query, which extends from
ObjectType
‣ Mutation types and their fields work
the same as other types, but the
resolvers will add/change rather
than read
‣ The type of a mutation field can be
an Object type with a return value
(e.g., id of added item)
// graphql.php
$schema = new Schema([
'query' => Types::query(),
'mutation' => Types::mutation()
]);
// App/Type/MutationType.php
$config = [
'name' => 'Mutation',
'fields' => [
]
];
Exercise #13
Create a mutation to add a speaker
1. Create the Mutation type in
App/type/Mutation.php
2. Add the Mutation type to the
schema in graphql.php
3. In Mutation.php, create an
addSpeaker field, which will use
DataSource::addSpeaker to add a
new speaker to the database.
'addSpeaker' => [
'type' => Types::listOf(Types::speaker()),
'args' => [
'name' => Type::nonNull(Type::string()),
'twitter' => Type::string()
],
'type' => new ObjectType([
'name' => 'CreateSpeakerOutput',
'fields' => [
'id' => ['type' => Type::int()]
]
]),
'description' => 'Adds a new conference',
'resolve' => function($root, $args) {
return DataSource::addSpeaker($args['name'],
isset($args['twitter']) ? $args['twitter'] : null);
}
]
Client-Side GraphQL
Client-side GraphQL is about writing queries to request data from
a GraphQL server with a defined schema.
Queries from JavaScript
‣ Queries are made via HTTP
requests to a single endpoint
‣ There are several libraries
available to manage
GraphQL on the client
query ConferenceNamesAndDates{
conferences {
name
dates
}
}
Lokka
a simple graphql client library
‣ A simple JavaScript library for sending GraphQL queries in JavaScript,
just like standard fetch or ajax requests
Exercise #14
Run a client-side query with Lokka
1. Open lokka.php and write
JavaScript to initialize the Lokka
client
2. Using the query method, run a
query and print it to the console
once the promise resolves
var client = new Lokka({
transport: new Transport('/graphql.php')
});
client.query(`<!-- QUERY HERE -->`).then(response =>
{ console.log(response); });
Apollo Client
complete data management solution
‣ Declarative API for queries and mutations
‣ Normalized client-side caching
‣ Combine local and remote data
‣ Features for pagination, error handling, refetching, and optimistic UI
‣ Client libraries for popular frontend frameworks (React.js, Angular, Vue), as
well as native Android and iOS applications
Exercise #15
Run a client-side query with Apollo
1. Open apollo.php and write
JavaScript to initialize the Apollo
client
2. Using the <Query> component, run
a query to request conferences, and
render them in a list
const client = new ApolloClient({
link: new HttpLink( { uri: "/graphql.php" }),
cache: new InMemoryCache()
});
const CONFERENCES_QUERY = gql`<!-- QUERY -->`;
ReactDOM.render(
<Query client={client} query={CONFERENCES_QUERY}>
{({ loading, error, data }) => {
if (loading) return 'Loading...';
if (error) return `Error!`;
return (
<ul>
{data.conferences.map(conference => (
<li key={conference.id}>
{conference.name}
</li>
))}
</ul>
);
}}
</Query>,
document.getElementById('root')
);
GraphQL makes it easier to make
more efficient queries between your
client and your server.
GraphQL provides new ways to think
about your APIs and the structure of
your application’s data.
Give it a try!
Thanks!
Andrew Rota
@AndrewRota
Resources
‣ graphql.org
‣ github.com/webonyx/graphql-php
‣ github.com/chentsulin/awesome-graphql
‣ Howtographql.com
Code: bit.ly/graphql-php-tutorial-repo
Glitch: bit.ly/graphql-php-glitch
Complete App: bit.ly/complete-graphql-php
Complete Code: bit.ly/complete-graphql-php-glitch
1 de 97

Recomendados

Introduction to GraphQLIntroduction to GraphQL
Introduction to GraphQLSangeeta Ashrit
484 vistas25 diapositivas
Better APIs with GraphQL Better APIs with GraphQL
Better APIs with GraphQL Josh Price
858 vistas52 diapositivas
REST vs GraphQLREST vs GraphQL
REST vs GraphQLSquareboat
486 vistas23 diapositivas
GraphQLGraphQL
GraphQLJoel Corrêa
4.6K vistas19 diapositivas

Más contenido relacionado

La actualidad más candente

An intro to GraphQLAn intro to GraphQL
An intro to GraphQLvaluebound
2K vistas14 diapositivas
GraphQL IntroductionGraphQL Introduction
GraphQL IntroductionSerge Huber
3.3K vistas25 diapositivas
React & GraphQLReact & GraphQL
React & GraphQLNikolas Burk
909 vistas51 diapositivas
 Intro to GraphQL Intro to GraphQL
Intro to GraphQLRakuten Group, Inc.
1.6K vistas16 diapositivas

La actualidad más candente(20)

An intro to GraphQLAn intro to GraphQL
An intro to GraphQL
valuebound2K vistas
GraphQL IntroductionGraphQL Introduction
GraphQL Introduction
Serge Huber3.3K vistas
Introduction to GraphQLIntroduction to GraphQL
Introduction to GraphQL
Amazon Web Services5K vistas
React & GraphQLReact & GraphQL
React & GraphQL
Nikolas Burk909 vistas
 Intro to GraphQL Intro to GraphQL
Intro to GraphQL
Rakuten Group, Inc.1.6K vistas
Introduction to GraphQLIntroduction to GraphQL
Introduction to GraphQL
Rodrigo Prates364 vistas
Intro GraphQLIntro GraphQL
Intro GraphQL
Simona Cotin262 vistas
Introduction to GraphQLIntroduction to GraphQL
Introduction to GraphQL
Amazon Web Services11.7K vistas
Building Modern APIs with GraphQLBuilding Modern APIs with GraphQL
Building Modern APIs with GraphQL
Amazon Web Services952 vistas
Rest api standards and best practicesRest api standards and best practices
Rest api standards and best practices
Ankita Mahajan7.2K vistas
How to GraphQLHow to GraphQL
How to GraphQL
Tomasz Bak1.2K vistas
Building a GraphQL API in PHPBuilding a GraphQL API in PHP
Building a GraphQL API in PHP
Andrew Rota2.2K vistas
GraphqlGraphql
Graphql
Niv Ben David619 vistas
Introduction to graphQLIntroduction to graphQL
Introduction to graphQL
Muhilvarnan V317 vistas
GraphQLGraphQL
GraphQL
Cédric GILLET2.2K vistas
Introduction to GraphQL: Mobile Week SFIntroduction to GraphQL: Mobile Week SF
Introduction to GraphQL: Mobile Week SF
Amazon Web Services306 vistas
GraphQL in SymfonyGraphQL in Symfony
GraphQL in Symfony
Bernd Alter7K vistas
Introduction to REST - APIIntroduction to REST - API
Introduction to REST - API
Chetan Gadodia1.1K vistas

Similar a Tutorial: Building a GraphQL API in PHP

Graphql usageGraphql usage
Graphql usageValentin Buryakov
413 vistas37 diapositivas
Modern APIs with GraphQLModern APIs with GraphQL
Modern APIs with GraphQLTaikai
199 vistas17 diapositivas

Similar a Tutorial: Building a GraphQL API in PHP(20)

Tutorial: Building a GraphQL API in PHP

  • 1. Building a GraphQL API in PHP @AndrewRota | Longhorn PHP 2019 Code: bit.ly/graphql-php-tutorial-repo Glitch: bit.ly/graphql-php-glitch Complete App: bit.ly/complete-graphql-php Complete Code: bit.ly/complete-graphql-php-glitch
  • 2. Today we’ll learn about GraphQL and build a GraphQL API in PHP which can be consumed with a JavaScript frontend.
  • 3. By the end of the workshop you will: - Understand what GraphQL is and how it works - Have built a GraphQL API in PHP - Know why GraphQL might be the right choice for your application
  • 5. Challenges traditional APIs ‣ Over-fetching data ‣ Under-fetching data, requiring multiple round-trips ‣ Time spent iterating on endpoints and expected data shape
  • 6. GraphQL offers an alternative architecture for developing efficient, easy to understand APIs.
  • 7. What is GraphQL? “GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.”
  • 8. { conferences { name dates } } "conferences": [ { "name": "Longhorn PHP", "dates": "May 2-4, 2019" } ]
  • 9. { conferences { name speakers { name twitter } } } { "conferences": [ { "name": "Longhorn PHP", "speakers": [ { "name": "Andrew Rota", "twitter": "https://twitter.com/AndrewRota" } ] } ] }
  • 10. Tutorial Agenda ❏ Intro to GraphQL Concepts ❏ Setup our PHP project ❏ Queries ❏ Types and Resolvers ❏ Deferred Resolvers ❏ Mutations ❏ GraphQL on the Frontend ❏ Q&A
  • 11. What we’re Building An API for querying a database of PHP conferences and speakers.
  • 12. What we’re Building An API for querying a database of PHP conferences and speakers.
  • 13. What we’re Building An API for querying a database of PHP conferences and speakers.
  • 15. Development Environment I recommend “Glitch,” a browser-based editor with a server environment that runs PHP code. bit.ly/graphql-php-glitch
  • 16. Development Environment You can also clone the repo and run it locally with PHP 7.2 w/ SQLite and Composer. Run: `composer install; composer run start --timeout=0`
  • 18. GraphQL ‣ Developed internally at Facebook in 2012 ‣ Open sourced in 2015 ‣ Spec: facebook.github.io/graphql/draft
  • 19. GraphQL Implementations ‣ GraphQL is technology agnostic for both client and server ‣ Client implementations: ‣ Server implementations:
  • 20. GraphQL Advantages ‣ Client requests exactly the shape of the data it needs ‣ Multiple resources can be queried in a single request ‣ API is defined with a strongly-typed schema ‣ Supports powerful tooling for developers ‣ Streamlines frontend ← → backend API conversations
  • 21. GraphQL in a web stack QueryClient (e.g., browser, mobile app) /graphql on PHP Server response Database
  • 23. Queries + Fields ‣ In GraphQL you make queries for fields on objects ‣ The response will have the same shape as the query query { conferences { name dates } } query field
  • 24. Fields ‣ Fields might be scalar values, or they might be other Objects. ‣ Fields can refer to Objects, and you can make a sub-selection for fields of these Objects. ‣ This lets you avoid making multiple requests for related resources query { conferences { name speakers { name } } } sub-selection
  • 25. Arguments ‣ You can pass named arguments to each field and nested object. { conference(name: "Longhorn PHP") { speakers { name } } } argument
  • 26. Variables ‣ Dynamic values can be passed into queries via variables query SearchConfs($name: String){ conferences(nameFilter:$name) { name } } {"name": "Longhorn"}
  • 27. EXERCISE #1 Write and run GraphQL queries 1. Go to https://graphql.github.io/swapi-g raphql 2. Run a query (use docs tab to explore graph). For example:
  • 28. Types + Schemas ‣ Every GraphQL service defines the a set of types that describe what data can be requested
  • 29. Types + Schemas ‣ GraphQL servers can be written in any language, so we describe types with a language-agnostic “GraphQL schema language” type Conference { name: String! url: String! description: String location: String dates: String! # List of speakers at this conference speakers: [Speaker] }
  • 30. Types + Schemas ‣ GraphQL servers can be written in any language, so we describe types with a language-agnostic “GraphQL schema language” ‣ Types include: object, scalar, list, enumeration, union, interface, and non-nullable. type Conference { name: String! url: String! description: String location: String dates: String! # List of speakers at this conference speakers: [Speaker] } non-nullable scalar type list of object types
  • 31. Query + Mutation Types ‣ There are two special types in every GraphQL schema: Query and Mutation ‣ Root fields you define on Query and Mutation are the entry points of requests type Query { # Returns conferences conferences: [Conference] # Returns speakers speakers: [Speaker] } root fields root type
  • 32. Queries ‣ Queries ask for for data; analogous to GET requests. ‣ GraphQL clients (e.g., browsers, mobile apps), make queries against a single GraphQL endpoint ‣ Operation name and type can be optional query ConferenceNamesAndDates{ conferences { name dates } } operation nameoperation type fields
  • 33. Mutations ‣ Mutations are for modifying data; analogous to POST/PUT/DELETE requests. ‣ They start with the mutation root type, and will often leverage arguments, but are otherwise the same as queries mutation { addSpeaker( name: "Andrew Rota", twitter: "https://twitter.com/andrewrota") { id } }
  • 35. Introspection ‣ A key feature of GraphQL is its introspection system ‣ You can ask any GraphQL schema about what queries it supports ‣ This unlocks opportunities for powerful tooling { "data": { "__schema": { "queryType": { "name": "Query" }, "types": [ { "kind": "OBJECT", "name": "Query", "description": null, "fields": [ { "name": "conferences", "description": "Returns a list of PHP conferences", "args": [], "type": { "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", "name": "Conference", "ofType": null } }, "isDeprecated": false, "deprecationReason": null } ] } ] } } }
  • 36. GraphiQL - in browser IDE for exploring GraphQL
  • 37. Voyager - Any GraphQL API as an interactive graph
  • 38. Graphql Playground - like GraphiQL, but more features
  • 39. PHPStorm JS GraphQL Plugin - IDE Integration
  • 40. EXERCISE #2 -- Discussion -- Explore an API with Voyager ● What did you notice about the data graph? ● How does the data compare or contrast with data in applications you work with?
  • 41. EXERCISE #2 Explore an API with Voyager 1. Go to apis.guru/graphql-voyager 2. Select and API and explore!
  • 42. GraphQL on the Server
  • 43. EXERCISE #3 Setup our PHP workspace with Glitch 1. Go to: bit.ly/graphql-php-glitch 2. Click “Remix to Edit” 3. Explore and run the project ● index.php: links to voyager and graphql-playground and JS clients ● graphql.php: the graphql endpoint
  • 44. EXERCISE #3 Modify the code and see it run in graphql_playground.php 1. In graphql.php, add “hello world” string to the output array 2. Test that the endpoint now returns that array in graphql.php
  • 46. webonyx/graphql-php Provides: ‣ Type primitives for your Type system ‣ Query parsing, validation, and execution against a Type system ‣ Type introspection ‣ Tools for deferred field resolution Feature-complete implementation of the GraphQL spec in PHP, inspired by Facebook’s original node-js reference library.
  • 47. webonyx/graphql-php Used by: ‣ Folkloreatelier/laravel-graphql ‣ overblog/GraphQLBundle (symfony) ‣ ivome/graphql-relay-php ‣ wp-graphql/wp-graphql ‣ tim-field/graphql-wp Feature-complete implementation of the GraphQL spec in PHP, inspired by Facebook’s original node-js reference library.
  • 48. EXERCISE #3: Install graphql-php 1. In the console, run: composer require webonyx/graphql-php; refresh; 2. Check composer.json for the webonyx/graphql-php entry
  • 49. Schemas start with the special root type, Query.
  • 50. Creating a Query Type ‣ ObjectType is a collection of fields ‣ Query has root fields, the entry points for queries. ‣ Each field must have a name and type. It can also have a resolve function, args, description, and other properties. use GraphQLTypeDefinitionObjectType; use GraphQLTypeDefinitionType; $queryType = new ObjectType([ 'name' => 'Query', 'fields' => [ 'message' => [ 'type' => Type::string(), 'resolve' => function ($root, $args) { return 'hello world'; } ], ] ]);
  • 51. Exercise #4 Creating the Query type 1. See the Query type configuration in App/Type/QueryType.php 2. Add a field “message” with a resolver that returns a string $config = [ 'name' => 'Query', 'fields' => [ 'message' => [ 'type' => Type::string(), 'resolve' => function () { return 'hello world!'; } ], ] ];
  • 52. The graphql.php endpoint should take a query, execute it against a schema, and return the data from resolvers.
  • 53. 1. Parse the query into an AST 2. Validate the query against the schema 3. Traverse the query, breadth first, and execute a resolver function for each field
  • 54. Facade Method for Query Execution ‣ graphql-php offers a facade for this process in the class GraphQLGraphQL use GraphQLGraphQL; $result = GraphQL::executeQuery( $schema, $queryString, $rootValue = null, $context = null, $variableValues = null, $operationName = null, $fieldResolver = null, $validationRules = null ); $serializableResult = $result->toArray();
  • 55. Exercise #5 Initialize Schema and Execute Query 1. Initialize Schema instance with Query type 2. Execute query and return result // graphql.php $schema = new Schema([ 'query' => Types::query() ]); $result = GraphQL::executeQuery( $schema, $data['query'], null, null, (array)$data['variables'] ); $output = $result->toArray($debug);
  • 56. Exercise #6 Add to the message field 1. Add an argument to the message field 2. Make the argument required (hint: use Type::nonNull) 3. Add a description to the field, and view it in docs tab of graphql_playground $config = [ 'name' => 'Query', 'fields' => [ 'message' => [ 'type' => Type::string(), 'args' => [ 'name' => Type::string(), ], 'resolve' => function ($root, $args) { return 'hello' . $args['name']; } ], ] ];
  • 57. Building a GraphQL server is primarily about structuring schema types, and then implementing their field resolvers
  • 58. The schema defines what queries can be made, what types of data can be requested, and the relationships between those types The resolver functions define how to get the data for each field. definition implementation
  • 59. Types graphql-php provides types, which are instances of classes from GraphQLTypeDefinition ○ ObjectType → collection of fields, each with their own type ○ ScalarType → built in scalars types: string, int, float, boolean, or id ○ InterfaceType → abstract type with a collection of fields ○ UnionType → abstract type which enumerates other possible object types ○ InputObjectType → like ObjectType, but for complex args inputted by user ○ EnumType → scalar type which is restricted to a set of allowed values ○ listOf → array (or Traversable) list of another type ○ nonNull → any type which is guaranteed to not be null
  • 60. Types graphql-php provides types, which are instances of classes from GraphQLTypeDefinition ○ ObjectType → collection of fields, each with their own type ○ ScalarType → built in scalars types: string, int, float, boolean, or id ○ InterfaceType → abstract type with a collection of fields ○ UnionType → abstract type which enumerates other possible object types ○ InputObjectType → like ObjectType, but for complex args inputted by user ○ EnumType → scalar type which is restricted to a set of allowed values ○ listOf → array (or Traversable) list of another type ○ nonNull → any type which is guaranteed to not be null
  • 61. Common pattern for adding new objects to the graph: 1. Define a custom object type 2. Add it to a new field on an existing object type 3. Write the field resolver function
  • 62. Custom Object Types ○ We can define custom types by extending the ObjectType ○ These types will be a collection of fields, which could be other custom or builtin types
  • 63. Type Registry Each type must only be a single instance in the Schema, so we can use a registry class with static functions to store each type. Our type registry is in App/Types.php Whenever we add a new type, we should also add it to the type registry. /** * @return QueryType */ public static function query() { return self::$query ?: (self::$query = new QueryType()); }
  • 64. Let’s create our first custom type
  • 65. Exercise #7 Add a conference type 1. Define the type for a conference in App/Type/ConferenceType.php 2. Add all of the type’s scalar fields (i.e., omit the more complex “speaker” type) 3. Add the type to the types registry (App/Types.php) $config = [ 'name' => 'Conference', 'fields' => [ 'id' => Type::nonNull(Types::int()), 'name' => Type::nonNull(Types::string()), 'url' => Type::nonNull(Types::string()), 'description' => Types::string(), 'location' => Types::string(), 'dates' => Type::nonNull(Types::string()) ] ];
  • 67. Exercise #8a Add conferences to root query 1. Add conferences field to the root query that returns all the conferences with DataSource::getConferences 2. Add an argument to filter by name, using DataSource::searchConferencesByName 'conferences' => [ 'type' => Types::listOf(Types::conference()), 'description' => 'List PHP Conferences', 'resolve' => function() { return DataSource::getConferences(); } ],
  • 68. Exercise #8b Add getConferenceById field 1. Create another root field for getting conferences, but with an argument to select conference by id using DataSource::getConferenceById 'conferences' => [ 'type' => Types::listOf(Types::conference()), 'description' => 'List PHP Conferences', 'resolve' => function() { return DataSource::getConferences(); } ],
  • 69. Data fetching happens in resolve functions.
  • 70. Resolvers ‣ Resolve function can be implemented however you’d like to get the data: SQL queries, cache, or another API. ‣ Start by implementing the resolver on the Query’s field ‣ For scalars, the return value will be the value of the field ‣ For object types, the return value will be passed on to nested fields // QueryType.php 'conferences' => [ 'type' => Types::listOf(Types::Conference()), 'description' => 'List PHP Conferences', 'resolve' => function() { return DataSource::getConferences(); } ],
  • 71. Default + Custom Resolvers ‣ The default field resolver for objects will return the value (or null) by key or property on the object ‣ Or you can implement a custom resolver at the type or field level itself ○ ‘resolveField’ on type ○ ‘resolve’ on field // SomeType.php 'fields' => [ // Default resolvers return property on object 'id' => Type::nonNull(Types::int()), 'name' => Type::nonNull(Types::string()), // Custom resolver function 'otherField' => ['type' => Types::string(), 'resolve' => function ($value) { return 'some other value'; }], // Field not on object, to be handled by resolveField 'formattedName' => Type::nonNull(Types::string()), ], // Custom resolveField function for type 'resolveField' => function($value, $args, $ctx, $info) { if ($info->fieldName === 'formattedName') { return $value->name . '!'; } return $value->{$info->fieldName}; }
  • 72. ...let’s take a closer look at a resolve function function($root, $args, $context, ResolveInfo $info) { return DataSource::getData($root->id); } root / parent result arguments app context query AST and other meta info
  • 73. Exercise #9 Add custom field resolvers 1. Add a custom field to the conference type that returns a formatted value not available on the original object. 2. Refactor to use the field-level resolve function and type-level resolveField function 'formattedName' => [ 'type' => Types::string(), 'resolve' => function($value) { return $value->name . '!!! 🎉'; } ]
  • 74. Let’s add another type and root field!
  • 75. Exercise #10 Add speakers type and fields 1. Add a Speakers type in App/Type/SpeakerType.php 2. Add a root `speakers` field in QueryType.php to get all speakers with DataSource::getSpeakers() 3. Add a speakers field to conference, and resolve data with DataSource::getSpeakersAtConference($id)
  • 76. Context ‣ A value to hold information shared by all resolvers, e.g., user data, request metadata, etc. ‣ Set initially as 4th argument to GraphQL::executeQuery() ‣ Available as the 3rd argument in all resolvers // graphql.php $result = GraphQL::executeQuery( $schema, $data['query'], null, $appContext, (array) $data['variables'] ); // resolver function function($value, $args, $context) { return $context->someValue; }
  • 77. Exercise #11 Add context 1. Add a context object in executeQuery with some global data (e.g., a hard coded username) 2. Output that context as part of a field resolver’s return value. // graphql.php class AppContext{ public $user; } // ... $appContext = new AppContext(); $appContext->user = 'something'; // ... $result = GraphQL::executeQuery( $schema, $data['query'], null, $appContext, (array) $data['variables'] );
  • 79. n+1 problem ‣ Data-fetching problem that occurs when you need to fetch related items in a one-to-many relationship { conferences { name speakers{ name } } }
  • 80. Solution: deferred resolvers ‣ We can use GraphQLDeferred to delay field resolution until we can make a single batch request for the data ‣ Once all non-deferred fields are resolved, graphql-php will call the wrapped closures ‣ If you have an environment that supports async operations (e.g., HHVM, ReactPHP, PHP threads), some fields can also resolve async. 'resolve' => function($root) { SpeakerCollection::add($root->id); return new Deferred(function() use ($root) { return SpeakerCollection::get($root->id); }); }
  • 81. Exercise #12 Implement a deferred resolver 1. Refactor speakers field on Conference type to use a deferred resolver 2. See App/SpeakerCollection.php and DataSource::selectSpeakers to see how this might be more efficient 'resolve' => function($root) { SpeakerCollection::add($root->id); return new Deferred(function() use ($root) { return SpeakerCollection::get($root->id); }); }
  • 83. So far we’ve been reading data with queries, but like any API, GraphQL can also manipulate data with mutations.
  • 84. Mutation Types ‣ Mutation is a special root type, just like Query, which extends from ObjectType ‣ Mutation types and their fields work the same as other types, but the resolvers will add/change rather than read ‣ The type of a mutation field can be an Object type with a return value (e.g., id of added item) // graphql.php $schema = new Schema([ 'query' => Types::query(), 'mutation' => Types::mutation() ]); // App/Type/MutationType.php $config = [ 'name' => 'Mutation', 'fields' => [ ] ];
  • 85. Exercise #13 Create a mutation to add a speaker 1. Create the Mutation type in App/type/Mutation.php 2. Add the Mutation type to the schema in graphql.php 3. In Mutation.php, create an addSpeaker field, which will use DataSource::addSpeaker to add a new speaker to the database. 'addSpeaker' => [ 'type' => Types::listOf(Types::speaker()), 'args' => [ 'name' => Type::nonNull(Type::string()), 'twitter' => Type::string() ], 'type' => new ObjectType([ 'name' => 'CreateSpeakerOutput', 'fields' => [ 'id' => ['type' => Type::int()] ] ]), 'description' => 'Adds a new conference', 'resolve' => function($root, $args) { return DataSource::addSpeaker($args['name'], isset($args['twitter']) ? $args['twitter'] : null); } ]
  • 87. Client-side GraphQL is about writing queries to request data from a GraphQL server with a defined schema.
  • 88. Queries from JavaScript ‣ Queries are made via HTTP requests to a single endpoint ‣ There are several libraries available to manage GraphQL on the client query ConferenceNamesAndDates{ conferences { name dates } }
  • 89. Lokka a simple graphql client library ‣ A simple JavaScript library for sending GraphQL queries in JavaScript, just like standard fetch or ajax requests
  • 90. Exercise #14 Run a client-side query with Lokka 1. Open lokka.php and write JavaScript to initialize the Lokka client 2. Using the query method, run a query and print it to the console once the promise resolves var client = new Lokka({ transport: new Transport('/graphql.php') }); client.query(`<!-- QUERY HERE -->`).then(response => { console.log(response); });
  • 91. Apollo Client complete data management solution ‣ Declarative API for queries and mutations ‣ Normalized client-side caching ‣ Combine local and remote data ‣ Features for pagination, error handling, refetching, and optimistic UI ‣ Client libraries for popular frontend frameworks (React.js, Angular, Vue), as well as native Android and iOS applications
  • 92. Exercise #15 Run a client-side query with Apollo 1. Open apollo.php and write JavaScript to initialize the Apollo client 2. Using the <Query> component, run a query to request conferences, and render them in a list const client = new ApolloClient({ link: new HttpLink( { uri: "/graphql.php" }), cache: new InMemoryCache() }); const CONFERENCES_QUERY = gql`<!-- QUERY -->`; ReactDOM.render( <Query client={client} query={CONFERENCES_QUERY}> {({ loading, error, data }) => { if (loading) return 'Loading...'; if (error) return `Error!`; return ( <ul> {data.conferences.map(conference => ( <li key={conference.id}> {conference.name} </li> ))} </ul> ); }} </Query>, document.getElementById('root') );
  • 93. GraphQL makes it easier to make more efficient queries between your client and your server.
  • 94. GraphQL provides new ways to think about your APIs and the structure of your application’s data.
  • 95. Give it a try!
  • 97. Andrew Rota @AndrewRota Resources ‣ graphql.org ‣ github.com/webonyx/graphql-php ‣ github.com/chentsulin/awesome-graphql ‣ Howtographql.com Code: bit.ly/graphql-php-tutorial-repo Glitch: bit.ly/graphql-php-glitch Complete App: bit.ly/complete-graphql-php Complete Code: bit.ly/complete-graphql-php-glitch