Se ha denunciado esta presentación.
Utilizamos tu perfil de LinkedIn y tus datos de actividad para personalizar los anuncios y mostrarte publicidad más relevante. Puedes cambiar tus preferencias de publicidad en cualquier momento.

Tutorial: Building a GraphQL API in PHP

Workshop delivered at Longhorn PHP 2019

  • Inicia sesión para ver los comentarios

Tutorial: Building a GraphQL API in PHP

  1. 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. 2. Today we’ll learn about GraphQL and build a GraphQL API in PHP which can be consumed with a JavaScript frontend.
  3. 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
  4. 4. Andrew Rota @AndrewRota Associate Director, Software Engineering
  5. 5. Challenges traditional APIs ‣ Over-fetching data ‣ Under-fetching data, requiring multiple round-trips ‣ Time spent iterating on endpoints and expected data shape
  6. 6. GraphQL offers an alternative architecture for developing efficient, easy to understand APIs.
  7. 7. What is GraphQL? “GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.”
  8. 8. { conferences { name dates } } "conferences": [ { "name": "Longhorn PHP", "dates": "May 2-4, 2019" } ]
  9. 9. { conferences { name speakers { name twitter } } } { "conferences": [ { "name": "Longhorn PHP", "speakers": [ { "name": "Andrew Rota", "twitter": "https://twitter.com/AndrewRota" } ] } ] }
  10. 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. 11. What we’re Building An API for querying a database of PHP conferences and speakers.
  12. 12. What we’re Building An API for querying a database of PHP conferences and speakers.
  13. 13. What we’re Building An API for querying a database of PHP conferences and speakers.
  14. 14. phpconferences.sqlite `conferences` table: `talks` table: `speakers` table:
  15. 15. Development Environment I recommend “Glitch,” a browser-based editor with a server environment that runs PHP code. bit.ly/graphql-php-glitch
  16. 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`
  17. 17. Introduction to GraphQL
  18. 18. GraphQL ‣ Developed internally at Facebook in 2012 ‣ Open sourced in 2015 ‣ Spec: facebook.github.io/graphql/draft
  19. 19. GraphQL Implementations ‣ GraphQL is technology agnostic for both client and server ‣ Client implementations: ‣ Server implementations:
  20. 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. 21. GraphQL in a web stack QueryClient (e.g., browser, mobile app) /graphql on PHP Server response Database
  22. 22. GraphQL Concepts
  23. 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. 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. 25. Arguments ‣ You can pass named arguments to each field and nested object. { conference(name: "Longhorn PHP") { speakers { name } } } argument
  26. 26. Variables ‣ Dynamic values can be passed into queries via variables query SearchConfs($name: String){ conferences(nameFilter:$name) { name } } {"name": "Longhorn"}
  27. 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. 28. Types + Schemas ‣ Every GraphQL service defines the a set of types that describe what data can be requested
  29. 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. 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. 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. 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. 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 } }
  34. 34. GraphQL Tooling
  35. 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. 36. GraphiQL - in browser IDE for exploring GraphQL
  37. 37. Voyager - Any GraphQL API as an interactive graph
  38. 38. Graphql Playground - like GraphiQL, but more features
  39. 39. PHPStorm JS GraphQL Plugin - IDE Integration
  40. 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. 41. EXERCISE #2 Explore an API with Voyager 1. Go to apis.guru/graphql-voyager 2. Select and API and explore!
  42. 42. GraphQL on the Server
  43. 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. 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
  45. 45. The GraphQL Server Library
  46. 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. 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. 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. 49. Schemas start with the special root type, Query.
  50. 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. 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. 52. The graphql.php endpoint should take a query, execute it against a schema, and return the data from resolvers.
  53. 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. 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. 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. 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. 57. Building a GraphQL server is primarily about structuring schema types, and then implementing their field resolvers
  58. 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. 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. 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. 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. 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. 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. 64. Let’s create our first custom type
  65. 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()) ] ];
  66. 66. phpconferences.sqlite `conferences` table: `talks` table: `speakers` table:
  67. 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. 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. 69. Data fetching happens in resolve functions.
  70. 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. 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. 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. 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. 74. Let’s add another type and root field!
  75. 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. 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. 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'] );
  78. 78. Performance
  79. 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. 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. 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); }); }
  82. 82. Mutations
  83. 83. So far we’ve been reading data with queries, but like any API, GraphQL can also manipulate data with mutations.
  84. 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. 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); } ]
  86. 86. Client-Side GraphQL
  87. 87. Client-side GraphQL is about writing queries to request data from a GraphQL server with a defined schema.
  88. 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. 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. 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. 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. 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. 93. GraphQL makes it easier to make more efficient queries between your client and your server.
  94. 94. GraphQL provides new ways to think about your APIs and the structure of your application’s data.
  95. 95. Give it a try!
  96. 96. Thanks!
  97. 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

×