This document discusses building a GraphQL API in PHP. It provides an overview of GraphQL concepts like queries, fields, types and schemas. It then outlines the steps to build a GraphQL API in PHP using the graphql-php library: 1. Define object types and the Query root type in the schema 2. Initialize the GraphQL schema instance 3. Execute GraphQL queries against the schema and return the result By following these steps, one can build an API for querying a database of PHP conferences and speakers to understand how to build GraphQL APIs in PHP.
Building a GraphQLAPI 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 learnabout GraphQL and build a GraphQL API in PHP which can be consumed with a JavaScript frontend.
3.
By the endof 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
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 analternative architecture for developing efficient, easy to understand APIs.
7.
What is GraphQL? “GraphQLis a query language for APIs and a runtime for fulfilling those queries with your existing data.”
Development Environment You canalso clone the repo and run it locally with PHP 7.2 w/ SQLite and Composer. Run: `composer install; composer run start --timeout=0`
GraphQL Implementations ‣ GraphQLis technology agnostic for both client and server ‣ Client implementations: ‣ Server implementations:
20.
GraphQL Advantages ‣ Clientrequests 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 aweb stack QueryClient (e.g., browser, mobile app) /graphql on PHP Server response Database
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 mightbe 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 canpass named arguments to each field and nested object. { conference(name: "Longhorn PHP") { speakers { name } } } argument
26.
Variables ‣ Dynamic valuescan be passed into queries via variables query SearchConfs($name: String){ conferences(nameFilter:$name) { name } } {"name": "Longhorn"}
27.
EXERCISE #1 Write andrun 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 + MutationTypes ‣ 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 askfor 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 arefor 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 } }
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 anAPI with Voyager 1. Go to apis.guru/graphql-voyager 2. Select and API and explore!
EXERCISE #3 Setup ourPHP 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 thecode 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
webonyx/graphql-php Provides: ‣ Type primitivesfor 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: Installgraphql-php 1. In the console, run: composer require webonyx/graphql-php; refresh; 2. Check composer.json for the webonyx/graphql-php entry
Creating a QueryType ‣ 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 theQuery 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 endpointshould take a query, execute it against a schema, and return the data from resolvers.
53.
1. Parse thequery 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 QueryExecution ‣ 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 Schemaand 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 tothe 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 GraphQLserver is primarily about structuring schema types, and then implementing their field resolvers
58.
The schema defineswhat 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 foradding 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 typemust 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()); }
Exercise #7 Add aconference 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()) ] ];
Exercise #8a Add conferencesto 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 getConferenceByIdfield 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(); } ],
Resolvers ‣ Resolve functioncan 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 + CustomResolvers ‣ 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 acloser 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 customfield 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 . '!!! 🎉'; } ]
Exercise #10 Add speakerstype 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 valueto 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'] );
n+1 problem ‣ Data-fetchingproblem 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 adeferred 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); }); }
So far we’vebeen reading data with queries, but like any API, GraphQL can also manipulate data with mutations.
84.
Mutation Types ‣ Mutationis 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 amutation 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 isabout 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 graphqlclient library ‣ A simple JavaScript library for sending GraphQL queries in JavaScript, just like standard fetch or ajax requests
90.
Exercise #14 Run aclient-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 datamanagement 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 aclient-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 iteasier to make more efficient queries between your client and your server.
94.
GraphQL provides newways to think about your APIs and the structure of your application’s data.