GraphQL met Laravel en Lighthouse

In de wereld van moderne webontwikkeling zijn efficiënte en flexibele API's van cruciaal belang.
Bij Endeavour maken we daarom gebruik van GraphQL. Met deze krachtige query-taal kunnen onze frontend ontwikkelaars exact de data opvragen die zij nodig hebben. Zo wordt er geen onnodige data over de lijn gestuurd en kunnen we efficient nieuwe componenten ontwikkelen, zonder aanpassingen aan de API. Daarbij biedt GraphQL een belangrijke stap op het gebied van standaardisatie. Zo is de communicatie tussen de backend en de frontend voorspelbaar geworden en kunnen we ons focussen op het ontwikkelen van de wensen van de klant.

Met Lighthouse is het opzetten van een GraphQL API in Laravel erg eenvoudig! Het open-source pakket geeft ons het raamwerk waarmee we gemakkelijk onze GraphQL API kunnen opzetten. Het is de missende schakel die verzoeken naar de API interpreteert en navigeert naar de juiste stukken code binnen onze Laravel applicatie.

Ik neem je mee in de installatie en configuratie en laat je zien hoe je snel en gemakkelijk een API opzet!

GraphQL concepten

Voordat we beginnen is het belangrijk om een aantal basisconcepten van GraphQL en Lighthouse uit te leggen. Een GraphQL API bestaat feitelijk maar uit één endpoint, standaard is dat /graphql. Elke request naar de API gebruikt de POST methode, waarbij de request body de volgende JSON-structuur heeft:

{
    "query": "...",
    "operationName": "...",
    "variables": { "myVariable": "someValue", ... }
}

Een GraphQL API geeft altijd een response met de volgende JSON-structuur, waarbij altijd een van de twee attributen aanwezig moet zijn:

{
  "data": { ... },
  "errors": [ ... ]
}

Types

In GraphQL zijn types een fundamenteel concept dat bepaalt welke soorten gegevens beschikbaar zijn in de API en hoe deze gegevens zijn gestructureerd. Elk GraphQL-schema is opgebouwd uit een set van deze types, die aangeven welke velden beschikbaar zijn en welk type waarde elk veld teruggeeft.

Er zijn verschillende soorten types in GraphQL:

  1. Scalar types: Dit zijn de basistypes, zoals Int, Float, String, Boolean, en ID.
  2. Object types: Deze representeren complexe gegevens en bestaan uit velden die elk een specifiek type hebben. Bijvoorbeeld een User type met velden zoals name (van type String) en age (van type Int).
  3. Query en Mutation types: Dit zijn de toegangspunten voor het ophalen en wijzigen van gegevens in een GraphQL API. Een Query type wordt gebruikt voor het opvragen van gegevens, terwijl een Mutation type bedoeld is voor het aanpassen van gegevens.
  4. Input types: Deze worden gebruikt om gegevens in te voeren bij mutations. Ze lijken op object types, maar worden specifiek gebruikt om invoerparameters te definiëren.

Types in GraphQL zorgen ervoor dat de API voorspelbaar en goed gedocumenteerd is, omdat elke query exact moet voldoen aan het type-schema dat is gedefinieerd.

Schema

Alle typedefinities bij elkaar noemen we het GraphQL Schema. Het is de blauwdruk van de API en beschrijft de structuur en functionaliteiten van de API. In Lighthouse bouwen we het schema op in het graphql/schema.grapqhl bestand dat we bij de installatie gaan genereren.

Directives

Directives zijn binnen Lighthouse de primaire manier om functionaliteiten aan onze GraphQL API toe te voegen. Ze zijn gemakkelijk te herkennen, omdat ze altijd beginnen met een @. Directives kunnen op verschillende plekken in het schema worden toegepast.

Installeren en configureren

Alright, het stukje theorie hebben we gehad. We kunnen beginnen met het installeren van de benodigde pakketjes!

Mocht je deze stap over willen slaan en direct aan de slag willen met het maken van queries en mutations, clone
dan deze repository en volg de instructies uit de readme.

We beginnen met het opzetten van een nieuw Laravel project en de installatie van Lighthouse.

composer create-project laravel/laravel dlf-graphql-example
cd dlf-graphql-example
composer require nuwave/lighthouse

Vervolgens publiceren we het schema.graphql bestand in het mapje graphql. In dit bestand definiëren we al onze queries en mutations, vergelijkbaar met een route bestand, zoals je die van Laravel kent.

php artisan vendor:publish --tag=lighthouse-schema

We helpen onze IDE een handje om de Lighthouse-specifieke syntax te begrijpen, door het genereren van een _lighthouse_ide_helper.php bestand.

php artisan lighthouse:ide-helper

Tot slot installeren we een interactieve GraphQL Playground, die we gaan gebruiken om API calls te maken. Deze is standaard te bereiken op http://<APP_URL>/graphiql.

composer require mll-lab/laravel-graphiql --dev

Data ophalen uit de GraphQL API

We zijn klaar om onze eerste API call maken! Onze basisinstallatie van Laravel en Lighthouse komt, out of the box, met twee queries om gebruikers op te halen. Deze vind je in graphql/schema.graphql.

type Query {
    "Find a single user by an identifying attribute."
    user(
      "Search by primary key."
      id: ID @eq @rules(apply: ["prohibits:email", "required_without:email"])

      "Search by email address."
      email: String @eq @rules(apply: ["prohibits:id", "required_without:id", "email"])
    ): User @find

    "List multiple users."
    users(
      "Filters by name. Accepts SQL LIKE wildcards `%` and `_`."
      name: String @where(operator: "like")
    ): [User!]! @paginate(defaultCount: 10)
}

Navigeer naar de GraphQL Playground en voer de users query uit:

Users query

De @paginate directive zorgt ervoor dat resultaten gepagineerd worden teruggegeven. Met de paginatorInfo kunnen we zien hoeveel resultaten er in totaal zijn en hoeveel pagina's er zijn. Vervang je deze directive met de @all directive, dan krijg je alle resultaten terug.

Meerdere queries in één request

Een van de grootste voordelen van GraphQL is het bundelen van meerdere queries. Met één request naar de API kan daarmee alle data opgevraagd worden die nodig is.

query {
    # Van de eerste tien gebruikers willen we de ids hebben
    users(first: 10, page: 1) {
        data {
            id
        }
    }

    # Van de gebruiker met id 1 willen we gedetailleerde informatie
    user(id: "1") {
        id
        name
        email
        created_at
        updated_at
    }
}

Data aanmaken via GraphQL API

We weten nu hoe we data ophalen, maar hoe bewerken we data via de API? In GraphQL doen we dat met mutations. We gaan een mutation maken waarmee we een nieuwe gebruiker kunnen maken.

Open het schema.graphql bestand in de graphql map en plak daarin de volgende code:

type Mutation {
    register(input: RegisterInput! @spread): User! @create
}

input RegisterInput {
    name: String!
    email: String! @rules(apply: ["email", "unique:users,email"])
    password: String! @rules(apply: ["min:8"])
}

Open nu weer de GraphQL playground en voer de mutation uit:

Create user mutation

Met een paar regels code hebben we een mutation aangemaakt, waarmee we nieuwe gebruikers kunnen registreren.

Geavanceerde use cases

Met de standaard directives komen we een heel eind, zonder ook maar een regel PHP-code te schrijven. Natuurlijk dekken deze lang niet alle denkbare scenarios en kun je ook jouw eigen logica schrijven.

Maatwerk mutation

Als voorbeeld nemen we een beheerpaneel voor admins. Via dit paneel moet het mogelijk zijn om het wachtwoord van een gebruiker te resetten. We willen zelf het nieuwe wachtwoord kunnen specificeren, maar als deze niet wordt meegegeven willen we dat de API een willekeurig wachtwoord genereert.

Zoals voorheen openen we het schema.graphql bestand en vervolgens voegen we de volgende code toe:

extend type Mutation {
    resetUserPassword(input: ResetUserPasswordInput! @spread): String! @field(resolver: "App\\GraphQL\\Mutations\\ResetUserPassword")
}

input ResetUserPasswordInput {
    id: ID! @rules(apply: ["exists:users,id"])
    password: String @rules(apply: ["min:8"])
}

Met de @field directive verwijzen we naar de PHP class die verantwoordelijk is voor het afhandelen van deze mutation. Deze class bestaat nog niet, dus laten we die creëren.

app/GraphQL/Mutations/ResetUserPassword.php:

<?php

declare(strict_types=1);

namespace App\GraphQL\Mutations;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class ResetUserPassword
{
    public function __invoke($_, array $args): string
    {
        $user     = User::findOrFail($args['id']);
        $password = $args['password'] ?? Str::random(8);

        $user->update([
            'password' => Hash::make($password),
        ]);

        // Logic for sending an email to the user here

        return "Wachtwoord is gereset naar {$password}.";
    }
}

Voer vervolgens de volgende API call uit en kijk wat er gebeurt!

Reset user password

Maatwerk query

Een query hoeft niet altijd iets uit een database terug te geven. Het kan bijvoorbeeld nuttig zijn om een query te hebben die het versienummer van de API teruggeeft, die wordt uitgelezen uit het composer.json bestand.

We doen dat door wederom gebruik te maken van de @field directive.

extend type Query {
    apiVersion: String! @field(resolver: "App\\GraphQL\\Queries\\ApiVersion")
}

Maak vervolgens de class aan die verantwoordelijk is voor het afhandelen van de query logica.

app/GraphQL/Queries/ApiVersion.php:

<?php

declare(strict_types=1);

namespace App\GraphQL\Queries;

class ApiVersion
{
    public function __invoke($_, array $args): string
    {
        $composerContents   = file_get_contents(dirname(__DIR__, 3) . DIRECTORY_SEPARATOR . 'composer.json');
        $composerAttributes = json_decode($composerContents, true);

        return $composerAttributes['version'] ?? 'onbekend';
    }
}

Zorg ervoor dat je composer.json bestand een version gedefinieerd heeft en probeer de query uit te voeren.

query {
    apiVersion 
}

Queries uitbreiden

Soms doen de standaard directives bijna wat je wil, maar wil je de uitgevoerde query kunnen beïnvloeden. Ik laat je zien hoe!

Laten we de users query weer als voorbeeld pakken. We willen de pagination behouden, maar we willen alleen gebruikers terugkrijgen die na een opgegeven datum zijn aangemaakt.

Open het schema.graphql bestand en pas de users query aan als volgt:

users(
  "Filters by name. Accepts SQL LIKE wildcards `%` and `_`."
  name: String @where(operator: "like")

  "Filters by created_at."
  createdAfter: DateTime
): [User!]! @paginate(defaultCount: 10, builder: "App\\GraphQL\\Builders\\UsersBuilder")

Maak daarna de custom builder class aan.

app/GraphQL/Builders/UsersBuilder.php:

<?php

declare(strict_types=1);

namespace App\GraphQL\Builders;

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;

class UsersBuilder
{
    public function __invoke($_, array $args): Builder
    {
        $builder = User::query();

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

        return $builder;
    }
}

De query accepteert nu, naast de argumenten voor de paginering, ook het createdAfter argument. Het argument wordt uitgelezen in de builder class, die de database query uitbreidt en vervolgens de eloquent query instantie teruggeeft.

query {
    users(first: 10, page: 1, createdAfter: "2024-09-18 11:10:00") {
        data {
            id
            name
            created_at
        }
    }
}

Authenticatie en autorisatie

Bij het uitlezen en aanpassen van gebruikers, zoals in de voorbeelden hierboven, is een solide authenticatie en autorisatie flow onmisbaar. Lighthouse biedt daarvoor een oplossing middels de @guard en de @can directives, die gebruik maken van Laravel's guards en policies. Een uitstekende plugin is joselfonseca/lighthouse-graphql-passport-auth, wanneer je gebruik wilt maken van Laravel Passport. Dankzij daniel-de-wit/lighthouse-sanctum kun je snel aan de slag met met Laravel Sanctum.

De diepte in

Een GraphQL API opzetten is met Lighthouse een fluitje van een cent. De behandelde scenarios geven je hopelijk een goede basis om mee te starten, maar Lighthouse is erg uitgebreid. De documentatie is een goed startpunt wanneer je verder de diepte in wil. Er zijn tevens tal van plugins die de standaardfunctionaliteiten van Lighthouse uitbreiden.

Voor vragen die niet in de documentatie behandeld zijn kun je altijd een bericht plaatsen op de Discussions sectie van Lighthouse's github pagina. Voel je tevens vrij om mij een berichtje te sturen op LinkedIn als je ergens niet uitkomt!

Over de auteur

Dit artikel werd geschreven door Dennis Koster, Lead Developer bij Endeavour en bestuurslid bij de Dutch Laravel Foundation. Endeavour is een van onze founding partners en expert op het gebied van GraphQL.