GraphQL no Laravel com Lighthouse

1. Introdução ao GraphQL e ao Lighthouse

GraphQL é uma linguagem de consulta para APIs que permite aos clientes solicitar exatamente os dados que precisam, eliminando problemas comuns do REST como over-fetching e under-fetching. Enquanto no REST você geralmente recebe estruturas fixas de dados, no GraphQL o cliente especifica os campos desejados em cada requisição.

O Lighthouse é um pacote PHP que traz GraphQL para Laravel de forma elegante. Ele adota uma abordagem schema-first, onde você primeiro define o esquema GraphQL em arquivos .graphql e depois conecta esses tipos aos modelos Eloquent. As principais vantagens incluem:

  • Integração nativa com Eloquent e suas relações
  • Geração automática de queries e mutations baseadas em diretivas
  • Suporte a validação Laravel, autenticação e autorização
  • Performance otimizada com prevenção do problema N+1

2. Instalação e Configuração Inicial

Para instalar o Lighthouse, execute:

composer require nuwave/lighthouse

Em seguida, publique os assets:

php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider"

Isso criará o diretório graphql/ na raiz do projeto com a seguinte estrutura:

graphql/
  schema.graphql  # Schema principal
  directives/     # Diretivas personalizadas
  enums/          # Definições de enums
  interfaces/     # Definições de interfaces

O arquivo de configuração config/lighthouse.php permite ajustar diversos aspectos:

<?php
return [
    'route' => [
        'prefix' => 'graphql',
        'middleware' => ['api']
    ],
    'schema' => [
        'register' => base_path('graphql/schema.graphql'),
    ],
    'cache' => [
        'enable' => env('LIGHTHOUSE_CACHE_ENABLE', false),
    ],
];

3. Definindo o Schema GraphQL

Vamos criar um schema básico para usuários e posts. Edite o arquivo graphql/schema.graphql:

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!
    user: User! @belongsTo
    comments: [Comment!]! @hasMany
    created_at: DateTime!
    updated_at: DateTime!
}

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

type Query {
    users: [User!]! @all
    user(id: ID! @eq): User @find
    posts: [Post!]! @paginate
}

type Mutation {
    createUser(name: String!, email: String!, password: String!): User! @create
    updateUser(id: ID!, name: String, email: String): User! @update
    deleteUser(id: ID!): User! @delete
}

As diretivas @hasMany e @belongsTo conectam automaticamente os relacionamentos Eloquent. O Lighthouse infere os nomes dos métodos nos modelos.

4. Queries: Consultando Dados

Com o schema acima, queries comuns funcionam imediatamente:

# Buscar todos os usuários com seus posts
query {
    users {
        id
        name
        posts {
            title
            content
        }
    }
}

# Buscar um usuário específico
query {
    user(id: 1) {
        name
        email
    }
}

# Paginação de posts
query {
    posts(first: 10, page: 1) {
        data {
            title
            user {
                name
            }
        }
        paginatorInfo {
            total
            currentPage
            lastPage
        }
    }
}

Para filtros e ordenação, adicione diretivas ao schema:

type Query {
    users(name: String @where(operator: "like")): [User!]! @all
    posts(orderBy: _ @orderBy(column: "created_at" direction: DESC)): [Post!]! @paginate
}

Isso permite consultas como:

query {
    users(name: "João") {
        id
        name
    }
    posts(orderBy: [{ column: CREATED_AT, order: DESC }]) {
        data {
            title
            created_at
        }
    }
}

5. Mutations: Criando, Atualizando e Deletando

As mutations automáticas já cobrem operações CRUD básicas. Para adicionar validação, use a diretiva @rules:

type Mutation {
    createPost(
        title: String! @rules(apply: ["required", "min:3", "max:255"])
        content: String! @rules(apply: ["required", "min:10"])
        user_id: ID! @rules(apply: ["exists:users,id"])
    ): Post! @create
}

Para mutations personalizadas com lógica de negócio, crie uma classe resolver:

<?php
namespace App\GraphQL\Mutations;

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

class CreatePostWithValidation
{
    public function __invoke($rootValue, array $args): Post
    {
        $validator = Validator::make($args, [
            'title' => 'required|unique:posts|max:255',
            'content' => 'required',
            'user_id' => 'required|exists:users,id'
        ]);

        if ($validator->fails()) {
            throw new \Exception($validator->errors()->first());
        }

        return Post::create([
            'title' => $args['title'],
            'content' => $args['content'],
            'user_id' => $args['user_id']
        ]);
    }
}

E registre no schema:

type Mutation {
    createPostWithValidation(
        title: String!
        content: String!
        user_id: ID!
    ): Post! @field(resolver: "App\\GraphQL\\Mutations\\CreatePostWithValidation")
}

6. Autenticação e Autorização

Para proteger campos com autenticação, use a diretiva @guard:

type Mutation {
    createPost(
        title: String!
        content: String!
    ): Post! @create @guard
}

type Query {
    myPosts: [Post!]! @all @guard
}

Para autorização baseada em políticas Laravel, use @can:

type Mutation {
    updatePost(id: ID!, title: String, content: String): Post! @update @can(ability: "update", find: "id")
    deletePost(id: ID!): Post! @delete @can(ability: "delete", find: "id")
}

Configure o middleware de autenticação no arquivo config/lighthouse.php:

'middleware' => [
    \Nuwave\Lighthouse\Http\Middleware\AcceptJson::class,
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'auth:sanctum',
],

7. Otimização e Boas Práticas

Para evitar o problema N+1, use as diretivas @with e @hasMany:

type Query {
    users: [User!]! @all @with(relation: "posts")
}

Para paginação eficiente, utilize @paginate com @first:

type Query {
    recentPosts: [Post!]! @paginate @orderBy(column: "created_at", direction: DESC)
    latestPost: Post @first @orderBy(column: "created_at", direction: DESC)
}

Testes com PHPUnit usando helpers do Lighthouse:

<?php
namespace Tests\Feature\GraphQL;

use Tests\TestCase;
use Nuwave\Lighthouse\Testing\MakesGraphQLRequests;

class PostTest extends TestCase
{
    use MakesGraphQLRequests;

    public function test_can_create_post()
    {
        $response = $this->graphQL('
            mutation {
                createPost(
                    title: "Test Post"
                    content: "This is a test post content"
                    user_id: 1
                ) {
                    id
                    title
                }
            }
        ');

        $response->assertJson([
            'data' => [
                'createPost' => [
                    'title' => 'Test Post'
                ]
            ]
        ]);
    }

    public function test_requires_authentication()
    {
        $this->graphQL('
            mutation {
                createPost(
                    title: "Unauthorized"
                    content: "Should fail"
                    user_id: 1
                ) {
                    id
                }
            }
        ')->assertGraphQLErrorMessage('Unauthenticated.');
    }
}

8. Deploy e Considerações Finais

Em produção, ative o cache de schema no .env:

LIGHTHOUSE_CACHE_ENABLE=true

Para versionamento, use namespaces no schema:

type Query {
    v1: V1Queries! @namespace(field: "App\\GraphQL\\Queries\\V1")
    v2: V2Queries! @namespace(field: "App\\GraphQL\\Queries\\V2")
}

Integre com ferramentas como GraphQL Playground ou Altair para testar sua API durante o desenvolvimento:

// config/lighthouse.php
'graphiql' => [
    'enabled' => env('GRAPHQL_PLAYGROUND_ENABLED', true),
],

O Lighthouse oferece uma experiência robusta para construir APIs GraphQL no Laravel. Sua abordagem schema-first, combinada com a integração profunda com Eloquent e as ferramentas de autenticação do Laravel, torna o desenvolvimento produtivo e o código limpo. Comece com tipos simples e vá adicionando complexidade gradualmente conforme sua aplicação cresce.

Referências