Advanced Laravel

API Platform: GraphQL with Laravel

18 min Lesson 33 of 40

API Platform: GraphQL with Laravel

GraphQL is a powerful alternative to REST APIs that allows clients to request exactly the data they need. This lesson covers implementing GraphQL in Laravel using Lighthouse PHP, a framework for serving GraphQL from Laravel applications.

Installing and Configuring Lighthouse

Lighthouse is the most popular GraphQL library for Laravel. It provides a declarative way to define your GraphQL schema and integrates seamlessly with Eloquent.

# Install Lighthouse and GraphQL Playground
composer require nuwave/lighthouse
composer require mll-lab/laravel-graphql-playground

# Publish configuration
php artisan vendor:publish --tag=lighthouse-config
php artisan vendor:publish --tag=lighthouse-schema

# Configuration in config/lighthouse.php
return [
    'route' => [
        'uri' => '/graphql',
        'middleware' => [
            \Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
        ],
    ],
    'schema' => [
        'register' => base_path('graphql/schema.graphql'),
    ],
];

# GraphQL Playground will be available at /graphql-playground
Tip: GraphQL Playground is an interactive IDE for testing your GraphQL queries. It provides auto-completion, documentation, and query history, making it essential for development.

Defining Your GraphQL Schema

The schema is the contract between your client and server. It defines what queries and mutations are available and what data types they return.

# graphql/schema.graphql

"A datetime string with format `Y-m-d H:i:s`"
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

type Query {
    "Get all users"
    users: [User!]! @all

    "Get a user by ID"
    user(id: ID! @eq): User @find

    "Search users by name"
    searchUsers(name: String! @where(operator: "like")): [User!]! @all

    "Get paginated posts"
    posts(first: Int!, page: Int): PostPaginator @paginate

    "Get current authenticated user"
    me: User @auth
}

type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]! @hasMany
    created_at: DateTime!
    updated_at: DateTime!
}

type Post {
    id: ID!
    title: String!
    content: String!
    author: User! @belongsTo
    comments: [Comment!]! @hasMany
    created_at: DateTime!
    updated_at: DateTime!
}

type Comment {
    id: ID!
    body: String!
    post: Post! @belongsTo
    user: User! @belongsTo
    created_at: DateTime!
}

type PostPaginator {
    data: [Post!]!
    paginatorInfo: PaginatorInfo!
}

type PaginatorInfo {
    currentPage: Int!
    lastPage: Int!
    total: Int!
    hasMorePages: Boolean!
}
Note: Lighthouse's directive system (like @all, @find, @paginate) automatically generates resolvers based on Eloquent conventions, drastically reducing boilerplate code.

Implementing Mutations

Mutations are GraphQL's way of modifying data. They're similar to POST, PUT, and DELETE in REST APIs.

# graphql/schema.graphql

type Mutation {
    "Create a new user"
    createUser(input: CreateUserInput! @spread): User @create

    "Update an existing user"
    updateUser(id: ID!, input: UpdateUserInput! @spread): User @update

    "Delete a user"
    deleteUser(id: ID! @whereKey): User @delete

    "Create a post"
    createPost(input: CreatePostInput!): Post @field(resolver: "PostMutator@create")

    "Login user"
    login(email: String!, password: String!): AuthPayload @field(resolver: "AuthMutator@login")

    "Logout user"
    logout: LogoutResponse @field(resolver: "AuthMutator@logout") @guard
}

input CreateUserInput {
    name: String! @rules(apply: ["required", "string", "max:255"])
    email: String! @rules(apply: ["required", "email", "unique:users,email"])
    password: String! @rules(apply: ["required", "min:8"]) @hash
}

input UpdateUserInput {
    name: String @rules(apply: ["string", "max:255"])
    email: String @rules(apply: ["email", "unique:users,email"])
}

input CreatePostInput {
    title: String! @rules(apply: ["required", "max:255"])
    content: String! @rules(apply: ["required"])
    author_id: ID! @rules(apply: ["required", "exists:users,id"])
}

type AuthPayload {
    access_token: String!
    token_type: String!
    expires_in: Int!
    user: User!
}

type LogoutResponse {
    message: String!
    status: String!
}

Creating Custom Resolvers

For complex logic, you'll need custom resolvers. These are PHP classes that handle specific queries or mutations.

<?php

// app/GraphQL/Mutations/PostMutator.php
namespace App\GraphQL\Mutations;

use App\Models\Post;
use Illuminate\Support\Facades\Auth;

class PostMutator
{
    public function create($rootValue, array $args)
    {
        $user = Auth::user();

        return Post::create([
            'title' => $args['input']['title'],
            'content' => $args['input']['content'],
            'author_id' => $user->id,
            'published_at' => now(),
        ]);
    }
}

// app/GraphQL/Mutations/AuthMutator.php
namespace App\GraphQL\Mutations;

use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthMutator
{
    public function login($rootValue, array $args)
    {
        $credentials = [
            'email' => $args['email'],
            'password' => $args['password'],
        ];

        if (!Auth::attempt($credentials)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $user = Auth::user();
        $token = $user->createToken('api-token')->plainTextToken;

        return [
            'access_token' => $token,
            'token_type' => 'Bearer',
            'expires_in' => 3600,
            'user' => $user,
        ];
    }

    public function logout()
    {
        Auth::user()->currentAccessToken()->delete();

        return [
            'message' => 'Successfully logged out',
            'status' => 'success',
        ];
    }
}

// app/GraphQL/Queries/UserQuery.php
namespace App\GraphQL\Queries;

use App\Models\User;

class UserQuery
{
    public function searchAdvanced($rootValue, array $args)
    {
        $query = User::query();

        if (isset($args['name'])) {
            $query->where('name', 'like', "%{$args['name']}%");
        }

        if (isset($args['email'])) {
            $query->where('email', 'like', "%{$args['email']}%");
        }

        if (isset($args['created_after'])) {
            $query->where('created_at', '>=', $args['created_after']);
        }

        return $query->get();
    }
}
Warning: Always validate and sanitize input in your resolvers. GraphQL doesn't provide built-in protection against SQL injection or XSS attacks. Use Laravel's validation rules and Eloquent for safe database operations.

Subscriptions for Real-Time Data

GraphQL subscriptions enable real-time communication between client and server using WebSockets.

# Install Laravel WebSockets
composer require beyondcode/laravel-websockets
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider"
php artisan migrate

# graphql/schema.graphql
type Subscription {
    "Listen for new posts"
    postCreated: Post

    "Listen for comments on a specific post"
    commentAdded(postId: ID!): Comment

    "Listen for user notifications"
    notificationSent: Notification @guard
}

type Notification {
    id: ID!
    type: String!
    title: String!
    message: String!
    user: User!
    read_at: DateTime
    created_at: DateTime!
}
<?php

// Broadcasting a subscription event
use Nuwave\Lighthouse\Execution\Utils\Subscription;

// When a new post is created
$post = Post::create($data);
Subscription::broadcast('postCreated', $post);

// When a comment is added
$comment = Comment::create($data);
Subscription::broadcast('commentAdded', $comment, [
    'postId' => $comment->post_id,
]);

// Filtering subscription data
// app/GraphQL/Subscriptions/CommentAdded.php
namespace App\GraphQL\Subscriptions;

use App\Models\Comment;
use Illuminate\Support\Facades\Auth;

class CommentAdded
{
    public function filter($subscriber, $value)
    {
        // Only send to users who have access to the post
        $comment = $value;
        return $comment->post->isAccessibleBy(Auth::user());
    }

    public function resolve($rootValue, $args, $context)
    {
        return $rootValue;
    }
}

Authorization in GraphQL

Implementing proper authorization is crucial for securing your GraphQL API.

# graphql/schema.graphql

type Query {
    "Admin only query"
    allUsers: [User!]! @all @guard @can(ability: "viewAny", model: "App\\Models\\User")

    "Get posts user can access"
    myPosts: [Post!]! @all @guard @inject(context: "user.id", name: "author_id")
}

type Mutation {
    "Update post (author only)"
    updatePost(id: ID!, input: UpdatePostInput!): Post
        @update
        @can(ability: "update", find: "id")
}

# Custom authorization in resolver
<?php

// app/Policies/PostPolicy.php
namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function update(User $user, Post $post)
    {
        return $user->id === $post->author_id;
    }

    public function delete(User $user, Post $post)
    {
        return $user->id === $post->author_id || $user->isAdmin();
    }
}

// app/GraphQL/Mutations/PostMutator.php
public function delete($rootValue, array $args)
{
    $post = Post::findOrFail($args['id']);

    $this->authorize('delete', $post);

    $post->delete();

    return $post;
}

Exercise 1: Blog GraphQL API

Create a complete GraphQL API for a blog system with the following features:

  • Queries: List posts with pagination, search posts by title/content, get post with comments
  • Mutations: Create/update/delete posts, add comments to posts, like/unlike posts
  • Implement authentication using Sanctum tokens
  • Add authorization to ensure only authors can edit their posts
  • Create a subscription for new comments on posts

Exercise 2: E-Commerce Product API

Build a GraphQL API for an e-commerce platform:

  • Define types for Products, Categories, Orders, and Users
  • Implement queries for product search with filtering (price range, category, rating)
  • Create mutations for cart management (add to cart, update quantity, checkout)
  • Add real-time inventory updates using subscriptions
  • Implement field-level authorization (hide prices for guests)

Exercise 3: Social Network GraphQL

Develop a social network GraphQL API with:

  • User profiles with posts, followers, and following relationships
  • News feed query that returns posts from followed users
  • Mutations for follow/unfollow, create posts, like/comment
  • Real-time notifications subscription
  • Implement cursor-based pagination for infinite scrolling
  • Add rate limiting to prevent spam