GraphQL Tutorial: How to Build a PHP App

ProfilePicture of Damien Filiatrault
Damien Filiatrault
Founder & CEO
PHP and GraphQL logos

Have you heard about GraphQL? Facebook open-sourced it in 2015 and I believe it is a much better way to establish contracts between a client and a server than REST. If, like me, you are always searching for the easiest way to develop and maintain code, this GraphQL Tutorial is for you.

In this article we will:

It’s worth noting that both the code and the application we are going to explore are open source, so you can use it as a foundation for your own work. With such new technologies, ‘best practices’ are constantly evolving. As a result, I have thoroughly researched and refactored all code used in this article and hope it can serve as example code to get you started on your GraphQL journey.

Note: I use the term ‘information’ and ‘informations’ throughout this article to describe, respectively, the singular and plural of an object. This stems from the article originally being written in Portuguese, where it makes sense to use the plural for the word ‘information’.

Table Of Contents

How I Ended up Testing GraphQL

I have been using React as my frontend for a while and was mostly happy with its performance. My apps became a lot cleaner thanks to React’s paradigm. Its beauty lies in the fact that you always have a predictable view for a specific model state. The headaches caused by most bind and listeners are not present, which results in clean and predictable code.

React focuses on rendering the view: having the app state as the starting point and as the single source of truth. To manage that app state I was using the Redux library. Since I started using these two libraries together, there has not been a single day where I’ve lost time tracking the cause of any unexpected view behaviors.

Even with all the above benefits, there was a fundamental problem with something out of the scope of these libraries: data integration with the server. Specifically, asynchronous data integration.

This is why I was looking for a way to integrate data into my React frontend. I tried Relay, but stopped, due to their lack of documentation at that time. I then came across the GraphQL client Apollo JS.

React Apollo Client

Apollo Client is an efficient data client that runs alongside your JS application and is responsible for managing the data and data integration with the server.

It comes loaded with features like queries, caching, mutations, optimistic UI, subscriptions, pagination, server-side rendering, and prefetching. What’s more: Apollo also benefits from an active and enthusiastic community supporting the product with full documentation and very active Slack channels.

The client does the ‘dirty work’ of making asynchronous queries to the server very well, normalizing the data received and saving that in a cache. It also takes care of rebuilding the data graph from the cache layer and injecting it to the view components (using high order components, a.k.a. HOCs). The syncing between server and client is so efficient that you will see areas on the screen that you did not expect updating to their correct values. That’s a huge time saver on the frontend.

Apollo is also able to issue mutations to the server using GraphQL, since communication with the server is taken care of by Apollo. This allows developers to concentrate more time on the view and business logic.

What is GraphQL?

GraphQL puts a lot of control onto the client, allowing it to make queries that specify the fields it wants to see and as well as the relations it wants. This reduces requests to the server dramatically.

The introspective aspect of the language allows validation of the request payloads and responses in the GraphQL layer. This imposes a good contract between client and server. It further allows for custom development tools to build great API documentation with very little human interaction. Indeed, all we need is to add description fields to the schema.

While building the server, I also found out that the GraphQL model imposes an elegant server architecture, allowing focus to be more on the business logic and less on the boilerplate code.

GraphQL has been evolving so fast that, since its recent inception, it already has implementations for JavaScript, Ruby, Python, Scala, Java, Clojure, GO, PHP, C#, .NET and other languages. It also has a rapidly growing ecosystem, with serverless services regularly popping up offering to use it as an interface.

The broader development community has been waiting a long time for a solution to replace the outdated REST. Modern applications are a lot more client-oriented because it makes a lot of sense to give clients more responsibility and flexibility on how they query server information.

I believe that the origins of GraphQL, within a large corporate entity, have given it an advantage. It means it has been rigorously tested in ‘real life’ scenarios (as opposed to academic ones) before being opened up to the developer community. This all contributes to its robustness and excellent fit with modern apps.

GraphQL vs REST

A good way to see the advantages of GraphQL is to see it in practice. Imagine, for example, that you want to retrieve a list of posts from a user using REST. You would probably access and endpoint as per the below query:

http://mydoma.in/api/user/45/posts

What information does that give us? What data will come from that request? We can’t know unless we look at the code or add documentation over it, using a tool like Swagger.

Now, let’s look at the same query in GraphQL:

GraphQL querying the posts field of the Query “namespace” with an operation named “UserPosts”

1query UserPosts{
2 posts(userId:45) {
3 id
4 title
5 text
6 createdAt
7 updatedAt
8 }
9}
10

Takeaway: GraphQL usually has only one endpoint and we make all requests within it with queries like this.

To find out what queries are available, we define a schema on the server. The server defines a schema that lists the queries, possible arguments and return types.

The schema for our example above would look something like this:

1# Queries.types.yml
2# ...
3 fields:
4 posts:
5 type: "[Post]"
6 args:
7 userId:
8 type: "ID!"
9 resolve: "@=service('app.resolver.posts').find(args)"
10# Post.types.yml
11Post:
12 type: object
13 config:
14 description: "An article posted in the blog."
15 fields:
16 id:
17 type: "ID!"
18 title:
19 type: "String"
20 text:
21 type: "String"
22 description: "The article's content"
23 createdAt:
24 type: "DateTime"
25 updatedAt:
26 type: "DateTime"
27 comments:
28 type: "[Comment]"
29
30

So this schema has a lot to declare:

As you can see, it’s similar to a Swagger file. The difference is that part of our development process is to declare that schema. And by declaring that we are also writing our docs. This is a big improvement over REST.

Takeaway: Documentation generated within the process is far more reliable for ‘up-to-dateness’.

Let’s imagine now that we need to access this same service from a mobile device to make a simpler list of all posts. We just need two fields: title and id.

How would we do that in REST? Well, you would need to pass a parameter to specify this return type and code it inside our controller. In GraphQL, all we need to update is a query to say what fields are needed.

This is a query to the posts field in the query namespace. It defines the userId argument and the fields it will need: id and title

1query UserPosts{
2 posts(userId:45) {
3 id
4 title
5 }
6}
7

This query will return an array of posts with only two fields: id and title. So we can now see another improvement over REST.

Takeaway: In GraphQL the client can specify in the query what fields are needed – so only those are returned in the response. This saves us the need to write another endpoint.

Let’s consider another scenario where we look at a list of articles and want the last 5 comments for each of them. In REST, we could do something like the below:

Requesting the posts of user 45 using REST

http://mydoma.in/api/user/45/posts

and then request each post to grab the comment list for all posts returned

http://mydoma.in/api/post/3/comments?last=5

http://mydoma.in/api/post/4/comments?last=5

http://mydoma.in/api/post/7/comments?last=5

Or we could develop a custom “impure” REST endpoint.

http://mydoma.in/api/user/45/postsWithComments?lastComments=5

Or we could add the parameter with=comments to tell that endpoint to nest the comments into the posts. This would require changes to the service.

http://mydoma.in/api/user/45/posts?with=comments&lastComments=5

As you can see, these are cumbersome solutions. Let’s now look at how we would do this in GraphQL:

Querying the posts field in the query namespace. We pass the argument userId=45 to say we want the posts of that user. We also pass all the required fields (id,title,text,…). One of these fields, comments is an object field, and that tells GraphQL to include that relation on the response. Please, notice that even this field (that is an edge or relation) receives its argument (last=5)

1query UserPosts{
2 posts(userId:45) {
3 id
4 title
5 text
6 createdAt
7 updatedAt
8 comments(last:5) {
9 id
10 text
11 user {
12 id
13 name
14 }
15 }
16 }
17}
18

That’s all that was needed was to add “comments” to my query.

In a GraphQL query, you can nest the fields you need, even if they are relations.

I also declared, in the parentheses after the comments field, that I needed the last 5 comments there. The naming is clearer because I did not need to come up with a weird ‘lastComments’ argument name.

You are even able to pass arguments to every field or relation inside a query.

The GraphQL query is a lot easier to understand than the cumbersome REST calls. We can look at it and see that we want the posts from user 45 as well as the last 5 comments of each post with the id and name of the user of each comment. Simple.

It is also very easy to implement server side. The posts resolver there does not need to know we want the post WITH the comments. This is because the comments relation has its own resolver and the GraphQL layer will call it on demand. In short, we don’t need to do anything different to return the post than when we return it nested with its comments.

With GraphQL the server code is clean and well-organized.

Hopefully, this section has given you a clear understanding of why I consider GraphQL more efficient that REST.

In the following sections, where we look at some real world examples, and you will start to understand some of the deeper intricacies of GraphQL.

Tutorial: Developing with GraphQL

I will guide you through the following examples in the same order I would follow when developing a new feature for an app.

About the Example App

All the following examples are from a Knowledge Management App. The main purpose of this app is to organize knowledge that is shared through Telegram.

The main screen, as shown below, is where we organize messages in specific threads or conversations and put tags on those threads.

Screenshot of the example PHP app using GraphQL

The following examples will be simple, as our goal is just to explain the overall architecture.

If you are interested in diving deeper, this App has some more complex data structures you could study. Including:

Everything can be found in the repository.

Installing the Code on Your Machine

All code and images are available in this article. But, I encourage you to clone the repo and look at the rest of the code.

Development Cycle

These are the steps I usually take when I add any new functionality to a system:

Defining the New Functionality

We are going to add extra information to the classification (tagging) of a thread in a specific subtopic.

In other words, we will be changing this:

Screenshot of the conversations feature of the PHP app

Into this:

Screenshot of the conversations feature of the PHP app update

In GraphQL, you can see the documentation of the server and also run queries and mutations in it. If you started your own server, it should be running under 127.0.0.1:8000/graphiql.

Let’s look at the mutation that is used to insert ‘Information’ (which is an entity in our system). It’s the relationship between a Thread and a Subtopic. You can also understand it as a tag.

The mutation is called “informationRegisterForThread”.

Below is the schema of that mutation field, as seen in the GraphQL tool. This is auto-generated and available as soon as you write the schema. Since writing the schema is a main part of the development process, you will always have up-to-date documentation on your API.

Screenshot of the mutation field in the GraphQL tool

You can see the mutation expects a required id (ID!) and also an InformationInput object. If you click on InformationInput, you will see its schema:

Screenshot of the schema on the mutation field in the GraphQL tool

Note: I like putting the noun before the verb in order to aggregate mutations in the docs. That’s a workaround I’ve found due to the non-nested characteristics of GraphQL mutations.

It might seem unnecessary to have a nested InformationInput object into those arguments because it now contains only one subtopicId field. It is, however, good practice when designing a mutation because you reserve names for future expansion of the schema and also simplify the API on the client.

And this will help us now because we need to add a new input field to that mutation, to register that extra ‘about’ text. We can add that field inside our InformationInput object. So let’s start by changing our schema:

Defining Schema in GraphQL

Below is the InformationInput type definition in our schema. We are going to add the ‘about’ field to it. It’s a String and is optional. It would be required if it was “String!”. We also refine the descriptions here. input-object is a specific object type to receive data.

1#InformationInput.types.yml
2
3# from ...
4
5InformationInput:
6 type: input-object
7 config:
8 fields:
9 subtopicId:
10 type: "ID!"
11
12# to ...
13
14InformationInput:
15 type: input-object
16 config:
17 description: "This represents the information added to classify or tag a thread"
18 fields:
19 about:
20 type: "String"
21 description: 'Extra information beyond the subtopic'
22 subtopicId:
23 type: "ID!"
24 description: 'The subtopic that represents where in the knowledge tree we classify the thread.'
25

We have now added the ‘about’ field and improved the documentation for the ‘description’ fields. Let’s look at these now:

Screenshot of the schema on the mutation field in the GraphQL tool update

If you click on ‘about’ and ‘subtopicId’, you will be able to read the descriptions added for those fields. We are writing our app and writing our API docs at the same time in the exact same place.

Now we may add a new field ‘about’ when calling our mutation. Since our new field is not mandatory, our app should still be running just fine.

Our schema is created! Before we actually implement the saving of that data, let’s do some Test-Driven Development (TDD). The schema is easily visible on the frontend and it’s ok to write it without testing. The resolver action should be tested though, so let’s look at that.

Writing Tests

Most of my tests run against the GraphQL layer because, this way, they also test the schema. When incorrect data is sent, errors are returned. To run the test correctly, you should clear the cache every time.

1bin/console cache:clear --env=test;phpunit tests/AppBundle/GraphQL/Informations/Mutations/InformationRegisterForThreadTest.php
2

Let’s look at the test we have in place. This test does not require a fixture. It creates all its required data: two subtopics (tags) and 3 threads. The createThread will create dummy messages and add them to threads. After that, it will add information to the thread. Information is the relation between a thread and a subtopic (a.k.a. tag).

After that, it will read the thread’s information objects and assert that two such objects were inserted into the thread with id $t1. It will also make some other assertions.

The upper case methods are the ones that will make direct calls to the GraphQL queries and mutations.

1# Tests\AppBundle\GraphQL\Informations\Mutations\InformationRegisterForThreadTest
2
3 function helper() {
4 return new InformationTestHelper($this);
5 }
6
7 /** @test */
8 public function shouldSaveSubtopicId()
9 {
10 $h = $this->helper();
11
12 $s1 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Planta"])('0.subtopics.0.id');
13 $s2 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Construcao"])('0.subtopics.1.id');
14
15 $t1 = $h->createThread();
16 $t2 = $h->createThread();
17 $t3 = $h->createThread();
18
19 $h->INFORMATION_REGISTER_FOR_THREAD([
20 'threadId'=>$t1,
21 'information'=>['subtopicId'=>$s1]
22 ]);
23
24 $h->INFORMATION_REGISTER_FOR_THREAD([
25 'threadId'=>$t1,
26 'information'=>['subtopicId'=>$s2]
27 ]);
28
29 $h->INFORMATION_REGISTER_FOR_THREAD([
30 'threadId'=>$t3,
31 'information'=>['subtopicId'=>$s2]
32 ]);
33
34 $informations = $h->THREAD(['id'=>$t1])('thread.informations');
35
36 $this->assertCount(2,$informations);
37 $this->assertEquals($s1,$informations[0]['subtopic']['id']);
38 $this->assertEquals($s2,$informations[1]['subtopic']['id']);
39
40 $informations = $h->THREAD(['id'=>$t2])('thread.informations');
41 $this->assertCount(0,$informations);
42
43 $informations = $h->THREAD(['id'=>$t3])('thread.informations');
44 $this->assertCount(1,$informations);
45
46 $this->assertEquals($s2,$informations[0]['subtopic']['id']);
47 }
48
49

Let’s write our test to add the ‘about’ data into our query and see if its value is returned back when we read the thread.

The helper (InformationTestHelper) is responsible for calling queries on the GraphQL layer and returning a function. It returns a function so that we can call it with a json path to grab what we need. This pattern, function returning a function, may seem a little tricky at first, but it is worth using because of the clarity we get from its output.

If we refactor a little, you will see what I’m talking about:

The fixture creation is refactored into createThreadsAndSubtopics. The call to:

1THREAD, with id=$t1 (..$h->THREAD(['id'=>$t1]..)
2

returns a function that we call again passing 3 path strings

1('thread.informations', 'thread.informations.0.subtopic.id', 'thread.informations.1.subtopic.id')
2

That will return those 3 values as an array that we then assign to $informations, $s1ReadId and $s2ReadId using the list function.

1/** @test */
2
3 function createThreadsAndSubtopics() {
4 $h = $this->helper();
5
6 $s1 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Planta"])('0.subtopics.0.id');
7 $s2 = $h->SUBTOPICS_REGISTER_FIRST_LEVEL(['name'=>"Construcao"])('0.subtopics.1.id');
8
9 $t1 = $h->createThread();
10 $t2 = $h->createThread();
11 $t3 = $h->createThread();
12
13 return [$s1,$s2,$t1,$t2,$t3];
14 }
15
16 public function shouldSaveSubtopicId()
17 {
18 $h = $this->helper();
19
20 list($s1,$s2,$t1,$t2,$t3) = $this->createThreadsAndSubtopics();
21
22 $h->INFORMATION_REGISTER_FOR_THREAD([
23 'threadId'=>$t1,
24 'information'=>['subtopicId'=>$s1]
25 ]);
26
27 $h->INFORMATION_REGISTER_FOR_THREAD([
28 'threadId'=>$t1,
29 'information'=>['subtopicId'=>$s2]
30 ]);
31
32 $h->INFORMATION_REGISTER_FOR_THREAD([
33 'threadId'=>$t3,
34 'information'=>['subtopicId'=>$s2]
35 ]);
36
37 list(
38 $informations,
39 $s1ReadId,
40 $s2ReadId
41 ) = $h->THREAD([
42 'id'=>$t1
43 ])(
44 'thread.informations',
45 'thread.informations.0.subtopic.id',
46 'thread.informations.1.subtopic.id'
47 );
48
49 $this->assertCount( 2 , $informations );
50 $this->assertEquals( $s1 , $s1ReadId );
51 $this->assertEquals( $s2 , $s2ReadId );
52
53
54 $this->assertCount(
55 0,
56 $h->THREAD(['id'=>$t2])('thread.informations')
57 );
58
59 $informations = $h->THREAD(['id'=>$t3])('thread.informations');
60 $this->assertCount(1,$informations);
61
62 $this->assertEquals($s2,$informations[0]['subtopic']['id']);
63 }
64

So this:

1$informations[1]['subtopic']['id']
2

Is returned as:

1$s2ReadId
2

In response to the query:

1'thread.informations.1.subtopic.id'
2

That query path was run by JsonPath into the response that came from the GraphQL layer. This happens on the second function call. Functions returning functions is not a usual pattern seen in PHP, but it’s a very powerful tool, used a lot in functional programming.

This might be a little confusing if it’s the first time you are looking at this pattern. So I encourage you to explore the test helper code to understand what is going on.

Now let’s test for the new field we are going to add. We will start by registering an INFORMATION object with data in the ‘about’ field. After that, we will load that thread back, query that field’s value and assert it is equal to the original string.

In this test, we will use the createThreadsAndSubtopics function to create some data. After that, we will call the informationRegisterForThread field in the mutation object defined in our schema. We do that using the INFORMATION_REGISTER_FOR_THREAD helper. In that call we pass two arguments: the threadId and the InformationInput object we just defined with the ‘about’ field. After that, we query the thread and use the path ‘thread.informations.0.about’ to grab the value of the ‘about’ field. We then check to see if the value was saved.

1# Tests\AppBundle\GraphQL\Informations\Mutations\InformationRegisterForThreadTest
2
3 /** @test */
4 public function shouldSaveAbout()
5 {
6 $h = $this->helper();
7
8 list($s1,$s2,$t1) = $this->createThreadsAndSubtopics();
9
10 $h->INFORMATION_REGISTER_FOR_THREAD([
11 'threadId'=>$t1,
12 'information'=>[ # information field
13 'subtopicId'=>$s1,
14 'about'=>'Nice information about that thing.' # about field
15 ]
16 ]);
17
18 $savedText = $h->THREAD(['id'=>$t1])( # query for the thread
19 'thread.informations.0.about' # query the result for that field
20 );
21
22 $this->assertEquals( 'Nice information about that thing.' , $savedText ); # check it
23 }
24

Take a Breath

Let’s recap a little. We first defined the functionality by making our user interface and by defining the input schema. After that we wrote a test saving and requesting the about field.

We still need to add that field in the query, persist that value on the db and add it to the response. Please notice we have changed the input schema, but not the query schema.

Now we will continue in a test driven way, solving the issues presented by the tests. It might seem a little counterintuitive to those not used to it, but in fact, it’s a powerful way to code, because you can be sure you are also adding useful code.

Passing The Test

If we run this test straight away, it will fail, saying that it “could not query the about field” on the response returned by the THREAD query’. It is requested by the path (‘thread.informations.0.about’), requesting for the about field on the first (index 0) information object on the thread.

Requesting the About Field

So, we need to request the about field on that query. Please remember we added that field to the input server schema, making it possible to be inserted. But we did not actually make it available to be queried. Nor was it requested it on the frontend query. To request it, we need to go into the THREAD query helper. This makes the call to the thread field in the query namespace and adds the ‘about’ field.

This code is also nice because you can see how the helper is written and how the thread query is written. Please notice that the processResponse method will return a function that can be called with paths to query the response data.

1# Tests\AppBundle\GraphQL\TestHelper.php
2
3 function THREAD($args = [],$getError = false, $initialPath = '') {
4 $q ='query thread($id:ID!){
5 thread(id:$id){
6 id
7 messages{
8 id
9 text
10 }
11 informations{
12 id
13 about # <-- ADDED THIS FIELD subtopic{ id } } } }'; return $this->runner->processResponse($q,$args,$getError,$initialPath);
14 }
15

After adding that, we will get this error:

1[message] => Cannot query field "about" on type "Information".
2

And that’s correct because we are requesting the  ‘about’ field, but it does not exist in the Information type. We added ‘about’ only to the InformationInput type to receive that data.

Now we need to add it to the Information GraphQL type.

1#Information.types.yml
2
3Information:
4 type: object
5 config:
6 description: "A tag, a classification of some thread, creating a relationship with a subtopic that indicates a specific subject."
7 fields:
8 id:
9 type: "ID!"
10 thread:
11 type: "Thread"
12 description: "Thread that receive this information tag"
13 subtopic:
14 type: "Subtopic"
15 description: "The related subtopic"
16 about:
17 type: "String"
18 description: "extra information"
19

Now that Information type on the server has the ‘about’ field, we run the test again and get the following message:

1Failed asserting that null matches expected 'Nice information about that thing.'.
2

From this, we can see that the GraphQL is ok because we did not get any validation errors from the data being sent or retrieved back.

The error we received is because, in fact, we have not yet persisted our data to the database. So, let’s work on the resolvers now.

Persist the About Field to the Database

We will add the field to our mutation resolver. And also add the field to our ORM as shown in the code below by the annotation.

1# 1 - AppBundle\Resolver\InformationsResolver
2
3 public function registerForThread($args)
4 {
5 $thread = $this->repo('AppBundle:Thread')->find( $args['threadId'] );
6 $subtopic = $this->repo('AppBundle:Subtopic')->find( $args['information']['subtopicId'] );
7 $about = $args['information']['about']; # here
8 $info = new Information();
9 $info->setSubtopic($subtopic);
10 $info->setAbout($about); # and here
11 $thread->addInformation($info);
12 $this->em->persist($info);
13 $this->em->flush();
14 $this->em->refresh($thread);
15 return $thread;
16 }
17
18#AppBundle\Entity\Information
19
20 @ORM\Column(name="about", type="text", nullable=true)
21

If we run the test again we get a ‘test successful’ message. We’ve done it!

Note: I encourage you to open SubtopicsTestHelper and follow the ‘proccessResponse’ method (Reis\GraphQLTestRunner\Runner\Runner::processGraphQL). There you will be able to see the GraphQL call happening and the json path component being wrapped in the returning function.

This GraphQL article from DeBergalis is a good reference for this type of development.

So, what have we done so far?

Each step logically follows the next and could also be written by what they achieve:

Other Uses For GraphQL Mutations

Currently, our test runs the informationRegisterForThread mutation and then uses the thread query to check for the inserted data. But, mutations can also return data. In fact, if we look carefully at them, mutations are identical to other queries. They have a return type and can specify the format of the returned data.

If we were making these calls from the client, we would be doing two queries: one for the mutation and another to retrieve the thread again. That’s why the return type of the mutation is Thread:

Screenshot of another mutation field in the GraphQL tool

The mutation is also a query on the thread. So, instead of calling the mutation and then calling the thread query, as per below….

1$h->INFORMATION_REGISTER_FOR_THREAD([
2 'threadId'=>$t1,
3 'information'=>[ # information field
4 'subtopicId'=>$s1,
5 'about'=>'Nice information about that thing.' # about field
6 ]
7]);
8
9$savedText = $h->THREAD(['id'=>$t1])( # query for the thread
10 'thread.informations.0.about' # query the result for that field
11);
12
13$this->assertEquals( 'Nice information about that thing.' , $savedText ); # check it
14

We use the thread returned by the mutation and query it’s return (json path) with the ‘informations.0.about’. See below.

1 list($savedAbout) = $h->INFORMATION_REGISTER_FOR_THREAD([
2 'threadId'=>$t1,
3 'information'=>[
4 'subtopicId'=>$s1,
5 'about'=>'Nice information about that thing.'
6 ]
7 ])('informations.0.about');
8
9 $this->assertEquals( 'Nice information about that thing.' , $savedAbout );
10

This will register the mutation and return the information required to update the client with only one request. With this method, even our test become cleaner!

Since the thread has an information collection, it could be any of the information in the array. So, how can we know which? We could return the information instead of returning the thread. But, that’s unnecessary as we could have some changes on the thread. In fact, we can create specific types for our mutation returns.

Creating a Specific Type to Our GraphQL Mutation’s Result

I was reading this article on how to design mutations and it enforces the argument that a mutation should always make its own type to return.  This makes a lot of sense because by doing this, we create a schema that’s more flexible and has a good extension point. Adding a field to a return type, like InformationRegisterForThreatReturn, is a lot easier than changing a mutation return type.

In our case, that would allow us to return the informationId and the thread. Once we do this we have:

1#Mutation.types.yml
2informationRegisterForThread:
3 type: "Thread"
4 args:
5 threadId:
6 type: "ID!"
7 information:
8 type: "InformationInput"
9 resolve: "@=service('app.resolver.informations').registerForThread(args)"
10

The result type is the ‘Thread’ type. This gives us almost no flexibility to add or remove ‘informations’ that might be needed there. So let’s create a specific return type for this mutation. This type is not a system entity representation, but is a representation of a response of this mutation. That gives us room to add extra information to this response without breaking a lot of code or the mutation contract. :

Note: Create a specific mutation return type for every mutation. That will give you flexibility on your schema evolution.

1# Mutation.types.yml - at the first level, after Mutation root field
2
3InformationRegisterForThreadResult:
4 type: object
5 config:
6 fields:
7 thread:
8 type: "Thread"
9

And change this type in our mutation:

1# Mutation.types.yml
2 informationRegisterForThread:
3 type: "InformationRegisterForThreadResult"
4 ...
5

Now we have added a layer of flexibility to our return. Let’s say we want to know the informationId of the information that was just inserted. We can do that easily by adding a new field to the mutation result. That won’t break anything on the client, due to the object returned. If the return was still the ‘Thread’ type, that would not be the case.

1# Mutation.types.yml - at the first level, after Mutation root field
2
3InformationRegisterForThreadResult:
4 type: object
5 config:
6 fields:
7 thread:
8 type: "Thread"
9 informationId:
10 type: "ID"
11

OK, we’re done. We’ve added some simple functionality to our system.

Components Overview

Here are the big components we used in the architecture for this app:

The GraphQL Layer, implemented using the OverblogGraphQLBundle.

The Testing Layer.

The Resolvers Layer, implementing business logic:

This next section should be used as a reference if you want to try this yourself.

Reference

To build the GraphQL server with Symfony, I used the Overblog GraphQL Bundle. It integrates the Webonyx GraphQL lib into a Symfony bundle, adding nice features to it.

One very special feature it has is the Expression Language. You can use it to declare the resolvers, specifying what service to use, what method to call and what args to pass like in the string below.

1# Mutation.types.yml
2
3resolve: "@=service('app.resolver.informations').registerForThread(args)"
4

The bundle also implements endpoints, improves the error handling and adds some other things tp the Webonyx GraphQL lib. One nice lib it requires is the overblog/graphql-php-generator that is responsible for reading a nice and clean yml file and converting it into the GraphQL type objects. This bundle adds usability and readability to the GraphQL lib and was, along with the Expression Language, the reason I decided to migrate from Youshido‘s lib.

GraphQL Schema Declaration

Our schema declaration is the entrance to the backend. It defines the API interface for the outer world.

It is located in src/AppBundle/Resources/config/graphql/  and queries are defined in the Query.types.yml and mutations are in Mutations.types.yml.

The resolvers are nicely defined there, along with the expression language we’ve talked about. To fulfill a request, more than one resolver can be called in the schema tree.

Disabling Cors to Test on Localhost

This app is not on a production server yet. To test it on localhost, I usually run the client on one port and the server on another, and I got some cors verification errors, so I installed the NelmioCorsBundle to allow that.

The configurations are very open and will need to be a lot more stringent on a prod server. I just wanted you to note that it’s running and will help you to avoid a lot of errors seen on the frontend client. If you don’t know this bundle yet, it’s worth taking a look at. It will manage the headers sent and especially the OPTIONS pre-flight requests to enable cross-origin resource sharing. In other words, will enable you to call your API from a different domain.

Error Handling

Error handling is a very open topic in the GraphQL world. The specs don’t say a lot (1,2) and are open to interpretation. As a result, there are a lot of different opinions on how to handle them.

Because GraphQL is so recent, there are no well-established best practices for it yet. Take this into consideration when designing your system. Maybe you can find a more efficient way of doing things. If so, test it and share with the community. Overblog deals with errors in a manner that is good enough for me. First, it will add normal validation errors when it encounters them during the validation of input or output types.

Second, it will handle the exceptions thrown in the resolvers. When it catches an exception, it will add an error to the response. Almost all exceptions are added as an “internal server error” generic message. The only two Exception types (and subtypes) that are not translated to this generic message are:

ErrorHandler::DEFAULT_USER_WARNING_CLASS

ErrorHandler::DEFAULT_USER_ERROR_CLASS

These can be configured in your config.yml with your own exceptions:

1overblog_graphql:
2 definitions:
3 exceptions:
4 types:
5 errors: "\\AppBundle\\Exceptions\\UserErrorException"
6

To understand it at a deeper level, you should look at the class that implements this handling:

1Overblog\GraphQLBundle\Error\ErrorHandler"
2

The ErrorHandler is also responsible for putting the stack trace on the response. It will only do this if Symfony’s debugging is turned on. That is normally done on the “$kernel = new AppKernel(‘dev’, true);” call.

I encourage you to test sending a non-existent id to that query with debug=true and with debug=false and see the response. Exceptions are logged on dev.log.

Final Thoughts

Having used GraphQL for several months, I can add to the chorus of positive reviews. It’s a query language that shows several key benefits over REST.

I would love to hear if this GraphQL Tutorial helped you progress with the development of your own apps or if you have any questions about my process. You can read more of our development articles here.

Are you looking to hire a PHP developer? Scalable Path specializes in matching companies with top-tier software developers. Hire your next developer now.

Originally published on Jun 28, 2017Last updated on Oct 28, 2022

Looking to hire?

Join our newsletter

Join thousands of subscribers already getting our original articles about software design and development. You will not receive any spam, just great content once a month.

 

Read Next

Browse Our Blog