Introduction
GraphQL1 is one of the recent major technologies introduced by Facebook in the React ecosystem. Essentially it is a declarative approach for querying data from a backend.
“React is changing the way we build complex client-side UI applications. Flux rethinks the flow of data through applications. GraphQL is changing the way we fetch our data from the server.”
In a nutshell, GraphQL is challenging REST. The key concept is that the client is the only component of an application that knows best what data is needed for rendering the UI. This means that each part of the UI can fetch declaratively from the server the exact data that it needs, without affecting other parts of the application.
I’m sure that this is a common scenario: you set up a REST endpoint for fetching the users of your application in the administrator section. Later you want to use the same endpoint in the public section, but soon you realize that you need only the name and surname of the users, but the endpoint returns a whole bunch of additional information. This is an overfetching problem, where unnecessary data is sent to the client each time it requests the list of users. You could decide to fetch only the data needed in the public section, but you will end up with an underfetching bug, affecting the admin rendering UI. Unfortunately REST doesn’t provide a solution out of the box. You could use complex url parameters to differentiate the requests or use different endpoints, but this will violate the REST principles, as well as introducing unwanted complexity to the application.
In most of the cases, even if your intention is to build a REST interface as flexible as possible, trying to avoid complex logic on your server, after some coding you realize that it cannot be done otherwise. So you end up building ad hoc endpoints that couple the data to particular views, violating the most important REST principle.
Facebook devs solved this problem using Relay, a sort of container that defines how a component should render (using React) and what data it needs to render (through a GraphQL representation), switching the logic from the server to the client.
“Relay takes care of composing the data queries into efficient batches, providing each component with exactly the data that it requested (and no more), updating those components when the data changes, and maintaining a client-side store (cache) of all data.”
In this way, if you change the data requirements of your components, you don’t need to touch the server. You can just change the data query and the rendering logic of the component and you’re good to go.
GraphQL
Let’s make an example of a GraphQL query for a Facebook comment (a slightly simplified version, as comment likes are not taken into account).
First we have to define what data it needs to render correctly:
{
"comment" : {
"id": 112654898798425,
"text": "We's liked it. Didn't we?",
"timestamp": "2015-12-09 14:52:55",
"parent_status": 1234567890,
"user" : {
"id": 4000,
"name": "Smeagol",
"isViewerFriend": true,
"profilePicture": {
"uri": "http://example.com/pic.jpg",
"width": 50,
"height": 50
}
}
}
}
A GraphQL query for the previous data could look like this:
{
comment(id : 112654898798425) {
id,
text,
timestamp,
parent_status,
user {
id,
name,
isViewerFriend,
profilePicture(size: 50) {
uri,
width,
height
}
}
}
}
Which basically means that you are interested in the comment with id
112654898798425, with fields id
, text
, timestamp
, parent_status
and the user
relationship with the defined fields.
To reach the same detail level in REST would mean encoding each field as a query parameter with additional syntax for dealing with relationships (user
and profilePicture
) and subqueries (size : 50
). You may end with something like this, using some sort of mapping with the Eloquent syntax:
/comments/112654898798425?
with=user,user.profilePicture
field=id&
field=text&
field=timestamp&
field=parent_status&
field=user.id&
field=user.name&
field=user.isViewerFriend&
field=user.profilePicture.uri&
field=user.profilePicture.width&
field=user.profilePicture.height
Not so expressive as the GraphQL syntax and it doesn’t even take into account the subquery on profilePicture
that returns only the picture of size 50.
So, following the official GraphQL introduction, we can outline the main benefits of GraphQL:
- Hierarchical data structure
- Human/Machine readable because the query is shaped like the data it returns
- Product(Component)-centric
- Backend independent, the specification is not tied to a particular programming language or storage engine
- Application-layer protocol, you can use it with HTTP or WebSockets for example
- Strongly-typed
Another benefit when using GraphQL instead of REST is that you can use a single HTTP end-point for all the requests.
GraphQL in Laravel
As we said, GraphQL is storage and backend independent therefore it principles can be implemented also in PHP and Laravel.
In this post we will use the Laravel 5 implementation by Folkloreatelier, which is based on the graphql-php library.
Warning: Both libraries are still work-in-progress, the use in production is discouraged.
The installation is straightforward, as any other Laravel package. You can publish the configuration file using the command
$ php artisan vendor:publish --provider="Folklore\GraphQL\GraphQLServiceProvider"
In the configuration file you can tweak most of the parameters of the library.
<?php
'prefix' => 'graphql',
'routes' => [
'query' => '/query',
'mutation' => '/mutation'
],
The prefix
and routes
parameters define the HTTP endpoints for the GraphQL queries. The above parameters make available to your application two distinct URLs (/graphql/query
and /graphql/mutation
), one for the queries and one for the mutations. I’ve only vaguely started thinking about mutations, so in this post I will consider only queries.
Next you need to create a type
for your queries. In this initial phase it’s safe to assume that each type corresponds to a Laravel Model in your application. A type
is essentially an object that defines the schema of a GraphQL query. Following the previous example we can write our CommentType
class:
<?php
class CommentType extends GraphQLType {
protected $attributes = [
'name' => 'Comment',
'description' => 'The Schema of a Comment Type'
];
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::id()),
'description' => 'The id of the comment'
],
'text' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The text of the comment'
],
'timestamp' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The timestamp of the comment'
],
'parent_status' => [
'type' => Type::nonNull(Type::int()),
'description' => 'The parent status of the comment'
]
];
}
}
As you can see, you can define which fields can be retrieved by the GraphQL request and their types.
Next you will need to add this class to the config/graphql.php
configuration file
<?php
'types' => [
'comments' => 'App\GraphQL\Type\CommentType'
]
Then you have to define a query that returns this type (or a list of them):
<?php
class CommentsQuery extends Query
{
protected $attributes = [
'name' => 'Comment query'
];
public function type()
{
return Type::listOf(GraphQL::type('comment'));
}
public function args()
{
return [
'id' => ['name' => 'id', 'type' => Type::int()]
];
}
public function resolve($root, $args, ResolveInfo $info)
{
if(isset($args['id'])) {
return Comment::where('id' , $args['id'])->get();
} else {
return Comment::all();
}
}
}
The code is straightforward. We are binding this query object to the comment
type and with the args
function we define the fields that can be used to resolve the type. In this example we can filter the request by passing an int
as an argument of the id
field.
Now you are able to fetch all your comments using this URL
graphql/query?query=query+FetchComments{comments+{id,text,timestamp}}
or fetch a single comment with
graphql/query?query=query+FetchComments{comments(id:112654898798425)+{id,text,timestamp}}
Does it test?
Unit tests on a GraphQL endpoint are relatively easy. I’m using the database migrations for each test case, which allow me to start with a new fresh database instance that I can seed based on my needs.
I noticed in the past that it’s quite challenging trying to mock Eloquent methods on models because most of the ones that we use to query are, in fact, methods of Illuminate\Database\Eloquent\Builder
and are called through __callStatic
and __call
.
So in this case I’m not using mocks, instead I’m calling the database each time in my tests because I’m able to define a context for my assertions using the model factory and the database migrations. This solution it’s acceptable for the purpose of this post, because the tests are not performed in true isolation.
<?php
class GraphQLCommentTest extends TestCase
{
use DatabaseMigrations;
public function testFetchComments() {
$comments = factory(App\Models\Comment::class)->create();
$this->get('/graphql/query?query=query+FetchComments{comments+{id,text,timestamp,parent_status}}')
->seeStatusCode(200)
->seeJsonEquals([
'data' => [
'comments' => [
[
'id' => $comment->id,
'text' => $comment->text,
'timestamp' => $comment->timestamp,
'parent_status' => $comment->parent_status
]
]
]
]);
}
}
What about relationships?
A relationship is just another field that corresponds to a type. If we follow our example we can create our UserType
like this
<?php
class UserType extends GraphQLType {
protected $attributes = [
'name' => 'User',
'description' => 'The Schema of a User Type'
];
public function fields()
{
return [
'id' => [
'type' => Type::nonNull(Type::id()),
'description' => 'The id of the user'
],
'name' => [
'type' => Type::nonNull(Type::string()),
'description' => 'The text of the comment'
],
'isViewerFriend' => [
'type' => Type::nonNull(Type::boolean()),
'description' => 'Is the viewer friend with this user'
]
];
}
}
Notice:
isViewerFriend
is not a field of our database, instead it is an accessor.
Next you have to define the relationship in the CommentType
by adding the following line to the fields
function
<?php
'user' => ['type' => GraphQL::type('user'), 'description' => 'The user relationship'],
Finally in the resolve
function of the CommentQuery
you can use Eloquent’s logic to eager load the relationship:
<?php
public function resolve($root, $args, ResolveInfo $info)
{
$fields = $info->getFieldSelection($depth = 3);
$comments = Comment::query();
foreach ($fields as $field => $keys) {
if($field === 'user') {
$comments->with('user');
}
}
if(isset($args['id'])) {
return $comments->where('id' , $args['id'])->get();
} else {
return $comments->get();
}
}
Basically we are retrieving the keys from the request using a method of the GraphQL\Type\Definition\ResolveInfo
instance class. Then we build the query by iterating through each field, loading the relationship if the user
key is present.
Sort results
GraphQL itself doesn’t have any notion of sorting. However you can easily implement the appropriate arguments and semantics that you want, in order to perform complex queries.
For example we can add an orderby
argument to the CommentQuery
class, as follows:
<?php
return [
'id' => ['name' => 'id', 'type' => Type::int()],
// [...]
'orderby' => ['name' => 'orderby', 'type' => Type::listOf(Type::string())]
];
Basically it means that our query now accepts an argument orderby
, defined as a list of strings.
You should also edit the resolve
function in order to use the new argument:
<?php
public function resolve($root, $args, ResolveInfo $info)
{
$comments = Comment::query();
if(isset($args['orderby']))
{
$comments->orderBy($args['orderby'][0], $args['orderby'][1]);
}
// [...]
}
Finally you can use this new field in our application like any other argument. For example the following URL is perfectly valid:
graphql/query?query=query+FetchComments+{comments(orderby:["id","DESC"])+{id,text,timestamp}}
In the next part I’ll dive into mutations. Stay tuned!
GraphQL is used in production by Facebook for nearly three years now, but just recently they decided to open-source a reference implementation. ↩︎