Using GraphQL in Laravel with Lighthouse PHP

Although I wrote about how to create a model and controller with one command line, it will be unlikely that I will need to do this for the API I build because I am actually going to be using GraphQL. I first used GraphQL last year when I maintained an API for a company and also built a new API with it for a messaging service to be used within the same company internally. To use GraphQL with Laravel I will be using the lighthouse-php framework.

GraphQL works a little different from REST. With REST, you have fixed defined endpoints of which a user can put in a request and get the data returned. Sometimes an endpoint can over fetch data, meaning that you get more than you ask for. Other times you could under fetch data which then might require you to fetch more data to get the remaining information you need. With GraphQL, you ask for the information you want and only that data is returned to you. Being open about this, I do not know yet if this is a huge bonus getting exactly what you need each time as it’s easy enough for frontend devs to get the fields they need from a response, but I like the way it works. I will also add that I have not done any performance tests on a heavily used system, although one day I would like to run some comparisons to see what performance is like side-by-side.

Describing the Data in GraphQL

In the API you describe your data with types. This description might look as follows:

type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
}

We are describing a user that has an ID, a name, an email, a created at and updated at date-time. These are all of the requirements to make a user model. The exclamation marks determine if the field is required or not with “!” meaning that it cannot be null. One thing to remember is that your migration for the database is likely to match this. If it’s nullable on the migration it’s almost always going to be nullable when describing the data.

With GraphQL you also have the ability to describe relationships with directives.

type Note {
    id: ID!
    user: User! @belongsTo
    ...

The user field is of type User and is required. Adding @belongsTo or another directive such as @belongsToMany describes that there is a relationship. The note belongs to the User. In the Laravel database migration this particular field would be defined this way:

$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');

You might have noticed that in the migration we use user_id and in the type, it is described as just user. This is handled in the input for the mutation which I will explain later, but in short, if we were to query a note we wouldn’t want just the ID of the user to be returned, such as 7; instead, we would want the actual user to be returned, or at least maybe the user’s name for example. This fulfils one purpose of GraphQL that we get everything we want rather than having to make a second query with that user_id.

GraphQL Queries

When the types have been created as well as the associated migrations to create the DB schema, the next step is to define the queries that can be done. For notes, we might have two queries. One to get all notes that match certain criteria such as who created it, or when it was created, and another where we can get the specific note.

extend type Query {
    notes(
        where: _ @whereConditions(columns: ["user_id", "note", "created_at"])
    ): [Note!]! @paginate @softDeletes
    note(id: Int! @eq): Note @find
}

The example above shows how a query might be defined in the API.

For notes, it uses the @whereConditions directive and will query the three specified columns in the database. On the client-side, the request can define how and what to search for in those three columns.

Line 4 shows [Note!]! which means that the result will be an array of notes. @paginate means it will be broken up into pages, and @softDeletes matches the migration when $table->softDeletes(); is defined (which means that items are not removed from the database, but just have a deleted_at field added). Adding the @softDeletes directive means that we only want to fetch notes that are not deleted.

GraphQL Mutations

So far I have explained briefly types and queries as well as made a brief mention of directives. I’ll cover directives in a moment, but next, we will look at mutations.

A mutation is how we manipulate or add data to the database. We need to define this in the API to let clients know what is required to create or update information. This looks as follows on the server-side:

extend type Mutation {
    createNote(input: createNoteInput! @spread): Note @create
    updateNote(input: updateNoteInput! @spread): Note @update
    deactivateNote(id: ID!): Note @delete
}

The code above defines the mutations which in the example above we can create a new note, update a note or deactivate a note. After the open bracket, we have input: which is like an argument. At this point, we are passing in createNoteInput! with the exclamation point telling us it is required. The input is described as follows:

input createMessageInput {
    user_id: Int! @rules(apply: ["required", "integer", "exists:users,id"])
    note: String! @rules(apply: ["required", "string"])
    user: createUserBelongsTo
}

When the mutation is used, we need to make sure we pass in all of the correct information as defined here. The users’ entry passes to another input called createUserBelongsToMany:

input createUserBelongsTo {
    create: [createUserInput!]
    update: [updateUserInput!]
    connect: [ID!]
    sync: [ID!]
}

This is where it all begins to feel complicated, but stick with it, many things in Lighthouse-PHP follow this format, so once you get it right for one thing, you will pretty much copy/paste and change a few field names to match the database migration. A lot of it is fairly boilerplate.

Custom GraphQL Directives, Events, Resolvers

Perhaps you want to add a new note to your account. You log in as a user, create a note, save it. This process would work great, but what if you wanted a user to register and then receive an email, or what if that user wanted to create a note and have it automatically inform subscribers by email? In this case we can use custom directives, events, or field resolvers. These are designed to enhance or customise what can be done with GraphQL.

In the createNote mutator you can add @event on to the end of it and specify a class to use and then have a default action performed in the construct:

createNote(input: createNoteInput! @spread): Note @create @event(dispatch: "App\\Events\\SendEmail")

In the SendEmail class you might have:

public function __construct($model)
{
    $email = new EmailController;
    $email->sendMessage($model);
}

You then can cause an email to be triggered in EmailController.

For the User type, you might want to hide the password on queries, but still show the password field. In this instance you might opt to create a custom directive that nulls that field when it is returned.

These parts can be quite tricky, but if you define when you should use an event, directive, or field resolver, you can typically find an example of how to accomplish what you need.

Querying Data in GraphQL

When everything is defined you can then begin to ask for data (or input data). An example query that fetches all notes for a specific user could be as follows:

query{
  user(id: 3){
    first_name
    last_name
    email
    notes {
      note
      created_at
    }
  }
}

We specify its a query, and then say that we want to query a user with the ID of 3. We want to know that users first name, last name, and email address. We then have a nested query for notes. Remember that a note belongs to a user, as defined in the type. This means we can grab all notes from the user. In this example we want the actual note and the date it was created_at (and all of them).

Running this query returns this:

{
  "data": {
    "user": {
      "first_name": "Ladarius",
      "last_name": "Windler",
      "email": "[email protected]",
      "notes": [
        {
          "note": "Vel sunt et corrupti ullam quia. Non eius voluptas rerum quia. Ratione debitis accusantium harum adipisci minima molestias magni ab. Explicabo ut dolor quam nulla dolorem aut.",
          "created_at": "2021-05-17 13:16:27"
        },
        {
          "note": "Ut odit possimus magni facilis. Autem voluptate qui repellendus dolores eos veniam. Soluta quia dolores quasi vel dolore et. Quis tenetur molestiae facere. Quo voluptas minus enim aspernatur consectetur veniam minus totam. Qui velit porro facere provident tempore illum. Harum hic in facilis aspernatur quibusdam magni nesciunt.",
          "created_at": "2020-08-01 09:29:52"
        }
      ]
    }
  }
}

We receive the response in JSON. You can copy and paste this in to an editor such as jsoneditoronline.org to more easily see the structure, but what we have is exactly what we asked for. We have the first and last name, email, and an array of notes (2 notes in this example), which has the note text and the date and time the note was created.

Using GraphQL Mutations

The previous example shows how to get data, but to create new data we need to use a mutation. A mutation looks something like this:

mutation{
  createNote(input: {
    note: "Test note"
    ...
  }){
    note
  }
}

We use createNote and then provide the input in curly braces. We simply make sure we add the required fields here so that we pass the right amount of information. Assuming all is valid, we pass the query and everything is handled by GraphQL.

My Folder Structure

When creating an API last year with Laravel and Lighthouse-PHP I used the following which followed the format that someone in the company used when creating another API:

I put the graphql folder in the app folder, and then have four folders in there which are Directives, Mutations, Queries, Types, and then in schema.graphql which is in the graphql folder I import *.graphql from each of those folders so that they can all be recognised correctly.

Other than the folder structure slightly differing from the standard lighthouse-php install, everything else in the beginning is standard. I’ll probably make a few changes to the lighthouse.php file in the config folder as I create the rest of the API, but for now, it’s good to get started.

Prep

Before you begin, I would recommend having the database designed beforehand. Having a fully designed database with all of the relationships makes creating an API with GraphQL quite simple. Because you know the relationships and all of the types, it’s mostly a matter of defining those and then adding in custom directives etc… as needed.

Where to Start

If you are ready to start I would set up a clean install of Laravel and then visit the Lighthouse-php website, particularly the getting started section. When ready, just experiment and see what breaks and what works. I think you’ll find that creating an API is fairly simple and powerful too.

Leave a comment