LaravelとGraphQLを使ったAPI開発

はじめに

現代のWebアプリケーション開発において、クライアントとサーバー間のデータ通信は非常に重要な要素です。従来のRESTful APIは広く使われていますが、柔軟性や効率性の面で課題も存在します。そこで注目されているのがGraphQLです。

GraphQLはFacebookが開発したAPIのためのクエリ言語であり、クライアントが必要なデータを正確に指定できるという特徴があります。この記事では、Laravel(PHP)とGraphQLを組み合わせてAPIを開発する方法について解説します。

GraphQLとは

GraphQLは、APIのためのクエリ言語であり、既存のデータに対するクエリを実行するためのランタイムです。RESTful APIと比較して、以下のような特徴があります:

  1. 必要なデータだけを取得可能: クライアントは必要なデータだけを指定して取得できるため、オーバーフェッチングを防ぎます。
  2. 一度のリクエストで複数リソースを取得: 複数のエンドポイントへのリクエストが不要になります。
  3. 強力な型システム: スキーマで型が定義されるため、型安全性が高まります。
  4. 自己文書化API: スキーマがそのままドキュメントとなります。
  5. 進化するAPI: 後方互換性を保ちながらAPIを発展させやすくなります。

開発環境のセットアップ

必要なもの

  • PHP 8.0以上
  • Composer
  • Laravel 9以上
  • GraphQLパッケージ(Lighthouse)

Laravelプロジェクトの作成

まずはLaravelプロジェクトを作成します。

composer create-project laravel/laravel laravel-graphql-api
cd laravel-graphql-api

Lighthouseのインストール

LaravelでGraphQLを実装するために、Lighthouseというパッケージを使用します。Lighthouseは、スキーマファースト開発を実現するLaravel用のGraphQLパッケージです。

composer require nuwave/lighthouse

設定ファイルの公開

Lighthouseの設定ファイルをプロジェクトに公開します。

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

これにより、config/lighthouse.phpgraphql/schema.graphqlファイルが作成されます。

データベースの設定

マイグレーションとモデルの作成

例として、ブログポストとそのカテゴリを管理するAPIを作成します。まずはマイグレーションとモデルを作成します。

php artisan make:model Category -m
php artisan make:model Post -m

database/migrations/xxxx_xx_xx_create_categories_table.phpを編集します:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('categories');
    }
};

database/migrations/xxxx_xx_xx_create_posts_table.phpを編集します:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('category_id')->constrained()->onDelete('cascade');
            $table->string('title');
            $table->text('content');
            $table->boolean('published')->default(false);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
};

モデルファイルを編集して、リレーションシップを定義します。

app/Models/Category.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'description'];

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

app/Models/Post.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use HasFactory;

    protected $fillable = ['category_id', 'title', 'content', 'published'];

    public function category()
    {
        return $this->belongsTo(Category::class);
    }
}

マイグレーションを実行します:

php artisan migrate

シードデータの作成(オプション)

開発をスムーズに進めるために、テストデータを作成することをおすすめします。

php artisan make:seeder CategorySeeder
php artisan make:seeder PostSeeder

database/seeders/CategorySeeder.php

<?php

namespace Database\Seeders;

use App\Models\Category;
use Illuminate\Database\Seeder;

class CategorySeeder extends Seeder
{
    public function run()
    {
        Category::create([
            'name' => 'Laravel',
            'description' => 'Laravel関連の記事'
        ]);

        Category::create([
            'name' => 'PHP',
            'description' => 'PHP関連の記事'
        ]);

        Category::create([
            'name' => 'GraphQL',
            'description' => 'GraphQL関連の記事'
        ]);
    }
}

database/seeders/PostSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Post;
use Illuminate\Database\Seeder;

class PostSeeder extends Seeder
{
    public function run()
    {
        Post::create([
            'category_id' => 1,
            'title' => 'Laravelの基本',
            'content' => 'Laravelは、PHPのモダンなWebアプリケーションフレームワークです。',
            'published' => true
        ]);

        Post::create([
            'category_id' => 3,
            'title' => 'GraphQLとは',
            'content' => 'GraphQLは、APIのためのクエリ言語です。',
            'published' => true
        ]);

        Post::create([
            'category_id' => 2,
            'title' => 'PHP 8の新機能',
            'content' => 'PHP 8では多くの新機能が追加されました。',
            'published' => false
        ]);
    }
}

database/seeders/DatabaseSeeder.phpを編集して、作成したシーダーを呼び出します:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $this->call([
            CategorySeeder::class,
            PostSeeder::class,
        ]);
    }
}

シードを実行します:

php artisan db:seed

GraphQLスキーマの定義

GraphQLでは、スキーマがAPIの構造を定義します。graphql/schema.graphqlファイルを編集して、必要な型とクエリを定義します。

"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

type Query {
    categories: [Category!]! @all
    category(id: ID! @eq): Category @find
    posts: [Post!]! @all
    post(id: ID! @eq): Post @find
    searchPosts(title: String @where(operator: "like")): [Post!]! @all
}

type Mutation {
    createCategory(
        name: String!
        description: String
    ): Category! @create

    updateCategory(
        id: ID!
        name: String
        description: String
    ): Category! @update

    deleteCategory(
        id: ID!
    ): Category! @delete

    createPost(
        category_id: ID!
        title: String!
        content: String!
        published: Boolean = false
    ): Post! @create

    updatePost(
        id: ID!
        category_id: ID
        title: String
        content: String
        published: Boolean
    ): Post! @update

    deletePost(
        id: ID!
    ): Post! @delete
}

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

type Post {
    id: ID!
    category: Category! @belongsTo
    title: String!
    content: String!
    published: Boolean!
    created_at: DateTime!
    updated_at: DateTime!
}

上記のスキーマでは、以下のことを定義しています:

  1. DateTimeスカラー型:日時を扱うためのカスタム型
  2. Query型:データを取得するための操作
    • 全カテゴリ取得
    • ID指定でカテゴリ取得
    • 全投稿取得
    • ID指定で投稿取得
    • タイトルで投稿検索
  3. Mutation型:データを変更するための操作
    • カテゴリの作成・更新・削除
    • 投稿の作成・更新・削除
  4. Category型:カテゴリのデータ構造
  5. Post型:投稿のデータ構造

Lighthouseのディレクティブ(@all@find@createなど)を使用することで、リゾルバーを自動的に生成しています。

GraphQL Playgroundのセットアップ

GraphQLのクエリをブラウザから簡単にテストするために、GraphQL Playgroundをセットアップします。

composer require mll-lab/laravel-graphql-playground

config/app.phpproviders配列に以下を追加します:

MLL\GraphQLPlayground\GraphQLPlaygroundServiceProvider::class,

これで、/graphql-playgroundにアクセスするとGraphQL Playgroundが表示されます。

APIのテスト

APIが正しく動作しているか確認するために、いくつかのクエリを試してみましょう。

カテゴリ一覧の取得

{
  categories {
    id
    name
    description
  }
}

投稿一覧の取得(カテゴリ情報含む)

{
  posts {
    id
    title
    content
    published
    category {
      id
      name
    }
  }
}

新しいカテゴリの作成

mutation {
  createCategory(
    name: "JavaScript"
    description: "JavaScript関連の記事"
  ) {
    id
    name
    description
  }
}

新しい投稿の作成

mutation {
  createPost(
    category_id: 4
    title: "JavaScriptの基本"
    content: "JavaScriptは、Web開発に欠かせない言語です。"
    published: true
  ) {
    id
    title
    category {
      name
    }
  }
}

投稿の更新

mutation {
  updatePost(
    id: 4
    title: "JavaScriptの基本と応用"
    content: "JavaScriptは、Web開発に欠かせない言語です。応用範囲も広がっています。"
  ) {
    id
    title
    content
  }
}

タイトルによる投稿の検索

{
  searchPosts(title: "%基本%") {
    id
    title
    category {
      name
    }
  }
}

認証と認可

実際のアプリケーションでは、APIアクセスに認証と認可が必要になることがほとんどです。Lighthouseでは、Laravelの認証システムと統合して、これらの機能を実装できます。

認証パッケージのインストール

composer require laravel/sanctum

マイグレーションを実行します:

php artisan migrate

スキーマに認証クエリを追加

graphql/schema.graphqlに認証関連のクエリを追加します:

extend type Mutation {
    login(
        email: String!
        password: String!
    ): AuthPayload!

    register(
        name: String!
        email: String!
        password: String!
    ): AuthPayload!

    logout: Boolean!
}

type AuthPayload {
    access_token: String!
    user: User!
}

type User {
    id: ID!
    name: String!
    email: String!
}

認証リゾルバーの作成

認証関連のクエリのリゾルバーを作成します:

php artisan lighthouse:mutation Login
php artisan lighthouse:mutation Register
php artisan lighthouse:mutation Logout

app/GraphQL/Mutations/Login.phpの内容:

<?php

namespace App\GraphQL\Mutations;

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

class Login
{
    public function __invoke($_, array $args)
    {
        $user = User::where('email', $args['email'])->first();

        if (!$user || !Hash::check($args['password'], $user->password)) {
            throw new \Exception('Invalid credentials');
        }

        return [
            'access_token' => $user->createToken('api_token')->plainTextToken,
            'user' => $user,
        ];
    }
}

app/GraphQL/Mutations/Register.phpの内容:

<?php

namespace App\GraphQL\Mutations;

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

class Register
{
    public function __invoke($_, array $args)
    {
        $user = User::create([
            'name' => $args['name'],
            'email' => $args['email'],
            'password' => Hash::make($args['password']),
        ]);

        return [
            'access_token' => $user->createToken('api_token')->plainTextToken,
            'user' => $user,
        ];
    }
}

app/GraphQL/Mutations/Logout.phpの内容:

<?php

namespace App\GraphQL\Mutations;

class Logout
{
    public function __invoke($_, array $args, $context)
    {
        $user = $context->user();
        
        if ($user) {
            $user->tokens()->delete();
            return true;
        }
        
        return false;
    }
}

スキーマに認証ガードを追加

一部のクエリやミューテーションに認証を要求するために、@guardディレクティブを使用します。graphql/schema.graphqlを編集します:

type Mutation {
    # 既存のミューテーション...

    # 認証が必要なミューテーション
    createPost(
        category_id: ID!
        title: String!
        content: String!
        published: Boolean = false
    ): Post! @create @guard

    updatePost(
        id: ID!
        category_id: ID
        title: String
        content: String
        published: Boolean
    ): Post! @update @guard

    deletePost(
        id: ID!
    ): Post! @delete @guard
}

これにより、createPostupdatePostdeletePostミューテーションは認証されたユーザーのみが実行できるようになります。

バリデーション

GraphQLのミューテーションにバリデーションを追加することも重要です。Lighthouseでは、@rulesディレクティブを使用してバリデーションを定義できます。

graphql/schema.graphqlを編集して、バリデーションルールを追加します:

type Mutation {
    createCategory(
        name: String! @rules(apply: ["required", "string", "max:255", "unique:categories,name"])
        description: String
    ): Category! @create

    updateCategory(
        id: ID!
        name: String @rules(apply: ["string", "max:255", "unique:categories,name"])
        description: String
    ): Category! @update

    createPost(
        category_id: ID! @rules(apply: ["required", "exists:categories,id"])
        title: String! @rules(apply: ["required", "string", "max:255"])
        content: String! @rules(apply: ["required", "string"])
        published: Boolean = false
    ): Post! @create @guard

    updatePost(
        id: ID!
        category_id: ID @rules(apply: ["exists:categories,id"])
        title: String @rules(apply: ["string", "max:255"])
        content: String @rules(apply: ["string"])
        published: Boolean
    ): Post! @update @guard
}

エラーハンドリング

GraphQLエラーを適切に処理するために、Lighthouseのエラーハンドリング機能を使用します。config/lighthouse.phpファイルでエラーハンドラーを設定できます。

デフォルトでは、GraphQLエラーはJSONレスポンスのerrors配列に含まれます。例えば、バリデーションエラーの場合:

{
  "errors": [
    {
      "message": "Validation failed for the field [createCategory].",
      "extensions": {
        "validation": {
          "name": [
            "The name has already been taken."
          ]
        },
        "category": "validation"
      }
    }
  ],
  "data": {
    "createCategory": null
  }
}

クライアントサイドの実装

GraphQL APIをフロントエンドから使用するには、さまざまなクライアントライブラリがあります。例えば、React用のApollo Clienturql、Vue.js用のvue-apolloなどがあります。

簡単な例として、fetchを使用してGraphQLクエリを実行する方法を示します:

async function fetchGraphQL(query, variables = {}) {
  const response = await fetch('/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      // 認証が必要な場合
      'Authorization': `Bearer ${localStorage.getItem('token')}`,
    },
    body: JSON.stringify({
      query,
      variables,
    }),
  });
  
  return response.json();
}

// 使用例:カテゴリ一覧を取得
fetchGraphQL(`
  query {
    categories {
      id
      name
      description
    }
  }
`).then(result => {
  console.log(result.data.categories);
});

// 使用例:ログイン
fetchGraphQL(`
  mutation($email: String!, $password: String!) {
    login(email: $email, password: $password) {
      access_token
      user {
        id
        name
        email
      }
    }
  }
`, {
  email: 'user@example.com',
  password: 'password'
}).then(result => {
  const token = result.data.login.access_token;
  localStorage.setItem('token', token);
});

パフォーマンス最適化

GraphQLでは、N+1クエリ問題が発生する可能性があります。例えば、複数の投稿を取得し、それぞれのカテゴリ情報も取得する場合、データベースクエリが増加します。

Lighthouseでは、この問題を解決するために@withディレクティブを使用できます:

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

また、キャッシュを使用してパフォーマンスを向上させることもできます。config/lighthouse.phpでキャッシュ設定を行います:

'cache' => [
    'enable' => env('LIGHTHOUSE_CACHE_ENABLE', true),
    'ttl' => env('LIGHTHOUSE_CACHE_TTL', 60),
],

運用環境での注意点

運用環境でGraphQL APIをデプロイする際には、以下の点に注意しましょう:

  1. Playgroundの無効化: 本番環境ではGraphQL Playgroundを無効にすることを検討してください。
  2. クエリの複雑さの制限: 非常に複雑なクエリはサーバーに負荷をかける可能性があります。クエリの複雑さを制限するミドルウェアを導入することを検討してください。
  3. レート制限: APIの乱用を防ぐために、レート制限を実装することをおすすめします。
  4. モニタリング: APIのパフォーマンスを監視し、問題があれば迅速に対応できるようにしましょう。

まとめ

この記事では、LaravelとGraphQLを使用したAPI開発の基本を解説しました。GraphQLの柔軟性と効率性は、特に複雑なデータ構造や多様なクライアントの要件に対応する必要がある場合に大きなメリットとなります。

Lighthouseパッケージを使用することで、LaravelでGraphQL APIを簡単に構築できます。スキーマファースト開発アプローチにより、APIの設計と実装が明確になり、フロントエンドとバックエンドの連携もスムーズに行えます。

GraphQLは比較的新しい技術ですが、その利点から多くのプロジェクトで採用されています。ぜひLaravelアプリケーションでGraphQLを試してみてください。

コメント

タイトルとURLをコピーしました