IPC Blog

Simple and practical: Laravel with GraphQL

Automatically generate a GraphQL interface with Lighthouse PHP and Eloquent

Jan 31, 2022

Lighthouse PHP is a framework for Laravel that simplifies the creation of GraphQL interfaces. Thanks to directives and automation, work for developers is reduced to the essentials: planning and data design. For GraphQL interface users, work is largely facilitated by autocompletion.

As a client, you determine which data is needed. The flood of data is in your hands. In this article, we will experience this directly in the GraphQL Playground. With this tool, you can easily create and test your own queries. Thanks to tools like the GraphQL Code Generator, using it in frontend frameworks like Vue.js is practically magic since commands for API communication be automatically generated. We will create a small blog as an example project. Basic knowledge of PHP, Composer, and Laravel are assumed.

Installation

Let’s start by launching a simple Laravel project. This process can vary depending on your operating system, so please refer to the official documentation [1]. In the next step, we add the framework Lighthouse using Composer. When using Laravel Sail this should happen in the shell (sail up -d && sail shell). You can see the procedure using Composer in the following:

composer require nuwave/lighthouse
php artisan vendor:publish --tag=lighthouse-schema

IPC NEWSLETTER

All news about PHP and web development

 

The second command generates a schema in the graphql directory. This describes the interface of GraphQL and at first, it contains two queries to read data from the model User. We will edit this file later. Next, install the GraphQL Playground. This is the best way to test the GraphQL interface:

composer require mll-lab/laravel-graphql-playground

This must also be set up, since we store the data in the MySQL database.

In the beginning, there is the data structure

For this example project, we’ll use a data structure that’s as simple as possible. We will use the following models:

  • Blog will contain a blog entry.
  • Author will represent a blog author.

We create the Model files and the Migration files, keeping them simple. We create, or rather prepare, the Author first since we will want to refer to it in the blog. In our case, a blog has a single author:

php artisan make:model Author -mf 
php artisan make:model Blog -mf

Only a few columns in our migration files are required for the necessary tables, which you can see in Listings 1 and 2.

database/migrations/...create_authors_table.php
public function up()
{
  Schema::create('authors', function (Blueprint $table) {
    // The author only has one name, 
    // an ID, creation and update date
    $table->id();
    $table->timestamps();
    $table->string('name');
  });
}

database/migrations/…create_blogs_table.php
public function up()
{
  Schema::create('blogs', function (Blueprint $table) {
    // The blog has a title, content, and author
    // an ID, creation and update date
    $table->id();
    $table->timestamps();
    $table->string('title');
    $table->mediumText('content');
    $table->foreignIdFor(\App\Models\Author::class);
  });
}

With php artisan migrate we can write the changes into the MySQL database. Now we have a relationship between Author and blog in the model (Listing 3), with something important for Lighthouse to take note of: The return type must be specified in the relationship function, or else Lighthouse cannot automatically recognize relationships (Listing 4).

app/Models/Author.php
namespace App;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
 
class Author extends Model
{
  public $fillable = ['name'];
 
  public function blog() : HasMany{
    return $this->hasMany(Blog::class);
  }
}

app/Models/Blog.php
namespace App;
 
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
class Blog extends Model
{
  public $fillable = ['title','content'];
 
  public function author() : BelongsTo{
    return $this->belongsTo(Author::class);
  }
}

This will give us a simple data structure we can experiment with. Optionally, we could create factory and seed classes to fill the tables with fake data. However, since we want to insert new entries into the tables with GraphQL later, we don’t need this right now.

 

First steps with the GraphQL interface

After defining our basic data structure, let’s open the GraphQL Playground at the URL http://localhost/graphql-playground. In the beginning, queries for users already exist in our schema, so let’s test if the playground works. After opening it, let’s write an opening curly bracket in the left field. This is shorthand for query QUERYNAME {} and means “I want to execute a query” Within this mode, pressing CTRL + SPACEBAR will open autocomplete. It should already show suggestions for all permitted queries (Fig. 1).

Fig. 1: Suggestions of all permitted queries

As a first query, we receive a list of all users. For this, users must be selected in the auto-completion. Alternatively, this can also be written out manually. Now we have to define which of the possible attributes we want to have back. This is GraphQL’s strength. As users of the API, we can greatly reduce traffic by specifying what we need. To select potential answers, we need to write a curly bracket again and activate autocompletion by pressing CTRL + SPACEBAR. Or, we can click on Docs in the right margin to see what’s possible there. Bit by bit, we build up our query (Listing 5).

query getFirstOfUsers {
  users {
    paginatorInfo{
      total
      currentPage
      lastPage
    }
    data {
      id
      email
    }
  }
}

users is not rendered completely by the @pagination directive, but with a scrolling function.

Therefore, in the type paginatorInfo you can get back the current page and the total number of entries. The return value is moved to data.

Hello World

We will now rewrite the schema and adapt it to our needs. Data writing is done by mutations; data query is done by queries. Open the file /graphql/schema.graphql and first, define a query that executes PHP code we wrote ourselves and returns its value as a response. In the schema, we enter it as:

type Query {
  hello(name: String): String!
}

This adds the query hello, guaranteeing that the response is always a string. The call sign marks the answer as “not null”. So, there will always be a string, and null will never be an answer. Additionally, we define the argument name. This is optional, as string has no callsign. Now, a string can be given to the name field when querying. But first, we create the response class /app/GraphQL/Queries/Hello.php using the shell. Now a basic framework has been created.

php artisan lighthouse:query Hello

In this basic framework, we can write PHP code that has an ending Return value corresponding to the specified type of queries. The transferred parameters are checked or in a checked state in the second argument’s array. The first argument contains data about the parent element, which in our case, is empty (Listing 6).

namespace App\GraphQL\Queries;
 
class Hello
{
  public function __invoke($_, array $args)
  {
    // Return the name or ‘World is the name is
    // not set
    return ($args['name'] ?? 'World') . '! ';
  }
}

Now, we can test this query in the Playground. We will still submit the name argument:

{
  hello(name: "Tim")
}

The answer we receive is:

{
  "data": {
    "hello": "Tim!"
  }
}

Providing models with GraphQL

Now we want to work with our own data model. For this, we’ll provide a list of all authors and blogs, as seen in Listing 7.

type Query {
  hello(name: String) :String!
    authors: [Author!]! @all
    blogs: [Blog!]! @all
}
 
type Blog {
  id: ID!
  title: String!
  content: String!
  author: Author!
  created_at: DateTime!
  updated_at: DateTime!
}
 
type Author {
  id: ID!
  name: String!
  created_at: DateTime!
  updated_at: DateTime!
  blogs: [Blog!]!
}

The square brackets stand for “an array of”. So, we want to create a list of authors and blogs, where no entry is null and at least one empty list is returned (Remember: the call sign stands for “not null”). Authors and blogs are defined with an additional one type each. This also lets us describe their relationship with each other. Lighthouse provides various directives that can change the function of the query or determine how the result is generated. These directives can be understood as markers that all start with an @ symbol. Here, we use @all to automatically populate the types with their associated models. Lighthouse will then try to infer the associated Model class based on the return value’s name. Here, the types are named Author and Blog. So, just as with the Model classes, this means that no additional info is needed. We can test our scheme directly in the Playground after saving it and, if necessary, fix any bugs.

IPC NEWSLETTER

All news about PHP and web development

 

Modify Models with mutations

Finally, let’s add the ability to add a new entry for a model using GraphQL:

type Mutation {
  createAuthor(name: String!): Author! @create
}

This happens automatically with the directive @create. The return value determines the model that will be created. All arguments are submitted to the model directly before saving. There are similar directives for update, delete, and upsert. Lighthouse also provides directives for validation and the documentation gives many examples of this. If the number of arguments becomes very large, it’s possible to collect them all in a separate type. Then, the directive @spread distributes the inner type as separate arguments to the function. We will exploit this in a moment.

type Mutation {
  createAuthor(input: CreateAuthorInput! @spread): Author! @create
}

input CreateAuthorInput {
  name: String!
}

We can execute the following query to test creating an author: 

mutation { 
  createAuthor(input: { 
      name: "Tim" 
  }) {
    id
    name
  }
}

In addition to this method, the return values are also defined at the end. In this case, we want to get back the ID and name. Lighthouse also gives us the possibility to create nested models at the same time. This allows us to save queries later on for more complex structures. This is achieved with a special syntax that varies depending on the relationship. The Lighthouse documentation explains this in detail [2]. As a final example, let’s generate a blog post like this and a matching author for it. First, we add to our schema, as seen in Listing 8.

type Mutation {
  createAuthor(input: CreateAuthorInput! @spread): Author! @create
  createBlog(input: CreateBlogInput! @spread): Blog! @create
}
 
input CreateBlogInput {
  title: String!
  content: String!
  author: CreateAuthorBelongsTo
}
 
input CreateAuthorBelongsTo {
  connect: ID
  create: CreateAuthorInput
}
 
input CreateAuthorInput {
  name: String!
}

Then, we test the mutation in the Playground again (Listing 9).

mutation {
  createBlog(input: {
    title: "Our Title"
    content: "Our first entry with GraphQL"
    author: {
      create: {
        name: "Tim"
      }
    }
  }) {
    id
    title
    content
    author {
      id
      name
    }
  }
}

If it’s successful, we get back the generated entry, as shown in Listing 10.

{
  "data": {
    "createBlog": {
      "id": "1",
      "title": "Our Title",
      "content": "Our first entry with GraphQL",
      "author": {
        "id": "1",
        "name": "Tim"
      }
    }
  }
}

All blog entries including the author can be returned with the following query:

query blogs { 
  blogs { 
    id 
    title
    content
    author {
      name
    }
  } 
}

YOU LOVE PHP?

Explore the PHP Core Track

 

Working with Lighthouse in the Frontend

In the following, we will use GraphQL automated in the React frontend. To do so, we must first generate the React framework with the slightly older Laravel UI. For npm, we must install Node.js. This is already the case in the container of Laravel Sail, for example. 

composer require laravel/ui
php artisan ui react
npm install && npm run dev

To view our React component, we update our welcome.blade.php as seen in Listing 11.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <link rel="stylesheet" href="/css/app.css"/>
</head>
<body>
  <div id="example"></div>
  <script src="/js/app.js"></script>
</body>
</html>

After updating the file with npm run watch, we can view the sample component by calling http://localhost. With Apollo, we access our data on the client side:

npm i @apollo/client graphql

Now, we can use Example.js to write our code. First, we need to initialize Apollo and build a basic framework for our application. We want to address the Hello query and directly display the server’s response (Listing 12).

import React, { useState } from 'react'
import ReactDOM from 'react-dom';
import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';
 
const client = new ApolloClient({
  uri: 'http://localhost/graphql',
  cache: new InMemoryCache()
});
 
function Example() {
  const [name, setName] = useState("");
  let answer = 'No answer from the server'
  return (
    <div className="container">
      <input
        type="text"
        value={name}
        placeholder="Name"
        onChange={e => setName(e.target.value)}
      />
      <div>
        { answer }
      </div>
    </div>
  );
}
 
export default Example;
 
if (document.getElementById('example')) {
  ReactDOM.render(
    <ApolloProvider client={client}>
      <Example />
    </ApolloProvider>, document.getElementById('example'));
}

With npm run watch, we compile the code and afterwards, we call the page in the browser. Now we can add our query, which we will specify and test in the playground. Here, we define expected arguments, in this instance: name as “to hand over”.

query hello($name: String) {
  hello(name: $name)
}

Initially, this seems a bit awkward. But errors are clearer if the query is named (in this instance, the first hello is the name) and arguments (in this case, $name) can be used multiple times in a query. In the application, we use the hook useQuery from Apollo. The advantage is that it returns the load status and potential errors directly, as shown in Listing 13.

import { ApolloProvider, ApolloClient, InMemoryCache, useQuery, gql } from '@apollo/client'
 
// ...
 
function Example() {
  const [name, setName] = useState("");
  const { loading, error, data } = useQuery(gql`
    query hello($name: String) {
      hello(name: $name)
    }
  `,{
    variables: {
      name
    }
  });
 
 
  return (
    <div className="container">
      {error}
      <input
        type="text"
        value={name}
        placeholder="Name"
        onChange={e => setName(e.target.value)}
      />
      <div>
        {loading && 'Think about...'}
        {data && (data?.hello ?? 'No answer from server') }
      </div>
    </div>
  );
}

The result of the server query is in data, where we want to deliver the contents of hello (the inner hello). With useQuery, the query is automatically fired off when it’s called for the first time or when the variables change. Besides useQuery, there is also useLazyQuery, where fetching data has to be manually triggered. For mutations, there is useMutation, which is similar in functionality as the previous two. As the name suggests, it handles mutations. There is even the possibility of uploading files.

 

Conclusion

Lighthouse PHP is very good for using GraphQL with Laravel. It saves a lot of work, especially in the PHP code since you do not have to write router, controller, and co. for every query.

Links & Literature

[1] https://laravel.com/docs/8.x/installation 

[2] https://lighthouse-php.com/master/eloquent/nested-mutations.html

Stay tuned!

Register for our newsletter

Behind the Tracks of IPC

PHP Core
Best practices & applications

General Web Development
Broader web development topics

Test & Performance
Software testing and performance improvements

Agile & People
Getting agile right is so important

Software Architecture
All about PHP frameworks, concepts &
environments

DevOps & Deployment
Learn about DevOps and transform your development pipeline

Content Management Systems
Sessions on content management systems

#slideless (pure coding)
See how technology really works

Web Security
All about
web security