Laravel Lighthouse: Implementing Graphql for Laravel

Laravel is a monolithic full stack framework that is widely used for it's powerful MVC design, exceptional ORM and great ecosystem. It has default bindings for implementing Vue inside it's templates. But recently we were searching for better ways to implement CRUD in our applications. Graphql is one of the powerful techniques in this area. Although it is mainly used with JavaScript ecosystem I think it creates a great partnership with Laravel.

Graphql is mainly a SDL for querying and modifying data in server. How it achieves that functionality is mainly dependent on platform graphql implementation. There is webonyx/graphql-php which Lighthouse built upon for PHP there is graphql-rust/juniper for Rust there is plenty of options for Javascript and so-on.

Lighthouse brings us integration with laravel ecosystem. This integration means that we can use Eloquent features, event and job dispatching, authorization through gates, and validation through our schemas direclty.

I will be creating a graphql api for a rough social media app and implement related bits and bobs along the way.

Models

First we need to define some basic models to reference them during building our schema

Our basic users table

id="Integer, Primary, Auto Increment"
name="String"
users table

Any social media app needs posts to stir up some emotion. Our posts will only include text messages and nothing more.

id="Integer, Primary, Auto Increment"
user_id="Integer, Foreign Key:users"
message="Short text"
posts table

Actions are users way of acting upon a posts. Likes and dislikes in our case but they can be extended.

id="Integer, Primary, Auto Increment"
user_id="Integer, Foreign Key:users"
post_id="Integer, Foreign Key:posts"
action="Enum:[LIKE, DISLIKE]"
actions table

Follows are our way of tracking who follows whom;

user_id="Integer, Foreign Key:users"
target_user_id="Integer, Foreign Key:users"
follows table

One of the requirement for creating models for lighthouse is that we need to mark our relationships in our models with correct. Laravel or PHP itself does not requires this but Lighthouse will not be able to access them correctly.

<?php

public function posts(): HasMany 
{
    return $this->hasMany(Post::class);
}

public function following(): BelongsToMany
{
    return $this->belongsToMany(User::class, 'user_id', 'target_user_id');
}

public function followed(): BelongsToMany
{
    return $this->belongsToMany(User::class, 'target_user_id', 'user_id');
}
App/Models/User.php
<?php

public function actions(): HasMany 
{
    return $this->hasMany(Action::class);
}

public function likes(): HasMany
{
    return $this->hasMany(Action::class)->action('LIKE');
}

public function dislikes(): HasMany
{
    return $this->hasMany(Action::class)->action('DISLIKE');
}
App/Models/Post.php
<?php

public function scopeAction(Builder $query, $action) 
{
    return $query->where('action', $action);
}
App/Models/Action.php

With our models ready we can start looking into creating our schemas.

Intro to Graphql

We can install graphql as per the docs. After installing and following the steps it will create us a basic schema that has a User type and basic queries.

Event thought we can create all of our models, queries and mutations in a single file Lighthouse gives us the ability to split them into files and folders and import them at will. I will be splitting schema into functional bricks and they will include relevant other schemas. In some areas this might be called domain driven design.

grapqhl/
├─ schema.graphql
├─ post/
│  ├─ action.graphql
│  ├─ post.graphql
├─ user/
│  ├─ user.graphql
Graphql folder structure

schema.graphql is the entry point for the Lighthouse it will use it first and will import anything that is referenced in it. We can use #import statements in our graphql files to import sub categories, files etc. So our schemas are light on feateures.

#import post/*.graphql
#import user/*.graphql
schema.graphql
extend type Query @guard {
    me: Profile @auth
}

type User {
    id: ID!
    name: String!
}
user/user.graphql

This is the basic schema. We have used some unknown things for graphql but Lighthouse provides us for like @guard and @auth. These are shorthand for laravel features. @guard makes sure that any request directed at the query block will be authenticated first before handled. @auth is a shorthand for Auth::user() in laravel. It will respond with our authenticated users information for our request.

We will define a Profile model too because I want to have a global user model that can be seen by every user and another one we provide for the authenticated user. We do this because we will add relations to our User model that should be invisible to third parties. Guarding these are somewhat convoluted because neither Graphql or Lighthouse provides us with a easier method. So splitting our model is easier.

type Profile @model(class: "App\\Models\\User") {
    id: ID!
    name: String!
    following: User @hasMany
    posts: Post @hasMany
}
user/user.graphql

I have added two @hasMany directives. These are Lighthouse's way of handling relationships. This relationships can be queried easily.

To show a posters information we need to add a query for user searching.

extend type Query @guard {
    user(id: ID! @eq): User @find    
}
user/user.graphql

This snippet will help us query a users information. @find will help us find a model with id: ID! @eq query provided here. We can rename id provide another column to search for etc.

Post

Social media needs posts to create post we need to create our first mutation. Before all let's create a schema for our post model.

extend type Query @guard {
    post(id: ID! @eq): Post @find    
    posts(orderBy: _ @orderBy(columns: ['id', 'created_at'])): [Post]! @paginate
}

type Post {
    user: User! @belongsTo
    message: String!
    likeCount: Int! @count(relation="likes")
    dislikeCount: Int! @count(relation="dislikes")
}
post/post.graphql

We came across our first @orderBy here. This directive is provided to us by lighthouse. Normally we have to provide any return or input type a concrete implementation but for orderBy we only provide a placeholder _ for it. Lighthouse will create a input type, ordering enums etc just for this field to use. This roughly expands into a schema like below.

type QueryPostOrderByClause {
    column: QueryPostOrderByColumn!
    order: SortOrder!
}

enum QueryPortOrderByColumn {
    ID: @enum(value: "id")
    CREATED_AT: @enum(value: "created_at")
}

Another directive we have used is @count this directive will calculate the count of the relation and provide to us if we need them. This directive can either count a relation or a model fully.

But what if want to post something for others to see. Well we can use Mutation's for that. Mutations are powerful component of the graphql environment. We can create, update, delete models with ease and coupled with Laravel and Lighthouse we can use and apply side effects on them.

extend type Mutation @guard {
    upsertPost(input: PostInput! @spread): Post 
        @upsert 
        @can(ability: "upsert") 
        @inject(context: "user.id", name: "user_id")
}

input PostInput {
    id: ID!
    message: String! @trim @rules(apply: ["required", "min:4"])
}
post/post.graphql

There is a lot to take in here like @upsert, @can, @inject, @trim, @rules and @spread. We want to process input such that after the request the data in our database should be formatted right, not tampered and consistent.

@upsert is a mutative directive. There are some others like @create, @update and @delete. This mutative directives will modify the data in our database with what is provided. But these directives need the data to be precise before they can work.

@spread is a formatting directive. Normally we can write all of our required fields right in the mutation body. But in long inputs or nested ones they might became unwieldy fast. Because of that we can define input types and use them in mutations or queries to simplify them. But this nested input.message alien to our database and our models. We need to remove it's encapsulation. @spread will mode everything in a input type to one level above it. This way our models can recognize it.

@trim is formatting directive. It will clear any preceeding and and trailing whitespace.

@rules is validation directive. It will check whether or not provided data fits into predefined validation rules. These rules can be any Laravel validation rule or custom rule. Or one can create a custom validation array to use. If validation fails the request will fail with an array of error messages.

@inject is a shorthand for GraphqlContext. We can access any values in it but it is generally used for user information fetching. We can ask user for it's id but then we have to check whether that id is truthfully theirs. This way we can add users id into input values easily.

@can is authorization directive. It will call Laravel Policies to check whether authenticated user is authorized to do requested action. We can provide extra information about, related key to find model, provided arguments, model name etc to it to customize authorization.