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"
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"
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]"
Follows are our way of tracking who follows whom;
user_id="Integer, Foreign Key:users"
target_user_id="Integer, Foreign Key:users"
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');
}
<?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');
}
<?php
public function scopeAction(Builder $query, $action)
{
return $query->where('action', $action);
}
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
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
extend type Query @guard {
me: Profile @auth
}
type User {
id: ID!
name: String!
}
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
}
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
}
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")
}
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"])
}
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.