Laravel + GraphQLでレッツメイクAPI (without Lighthouse)
はじめに
今回はLaravelとGraphQLを用いた簡易的なAPIを作成する手順を投稿します。
LaravelでGraphQLを扱う際によく使用されるライブラリとして、Lighthouse
というものがありますが、
今回はLighthouseではなく、graphql-laravel
というコードファーストなライブラリでAPIを作っていきます。
※当記事では簡易的なキャッチアップにとどめています。より深堀りたい方はドキュメントなどをご参照くださいませ。
使用する環境
- MacBook Air (M1, 2020)
- Laravel : 9
- graphql-laravel : 8.3
- IDE Helper : 2.12
- Doker : 20.10.17
※本記事ではローカル環境にDokerがインストールされていることが前提となります。
Laravel, graphql-laravel, IDE Helperは記事の中でインストールしていきます。
Laravel環境構築
Laravel Sailの導入
今回は簡単なLaravel環境構築として、Laravel Sailを使用します。
自身の作業ディレクトリ内、ターミナル上で以下コマンドを実行してください。
% curl -s https://laravel.build/gymnast-team | bash
build/
から後ろの部分は好きなプロジェクト名を入力できます。
今回のプロジェクト名はわかりやすいようにgymnast-team
としました。
環境によっては、インストールの途中でパスワードを要求される場合がありますので、ローカルマシンに設定しているパスワードを入力します。
インストールが完了したら以下コマンドを実行し、sailを起動します。
% cd gymnast-team && ./vendor/bin/sail up
公式ドキュメントにもある通り、初めて./vendor/bin/sail up
を実行すると、Sailのアプリケーションコンテナがローカル環境に構築されるため、数分時間を要しますが、以降のSailの開始・起動は高速になります。
Laravel環境がうまく起動できているか確認してみます。
http://localhost にアクセスして以下のような画面が出てくれば成功です。
パッケージのインストール
IDE Helper
を導入します。
IDE Helperとは、IDE(統合開発環境)の補完用ヘルパーファイルを生成してくれるパッケージ
です。
簡単にいうと、コードを補完してくれるお助けパッケージ的な感じです。
以下のコマンドでインストールしてください。
% sail composer require –dev barryvdh/laravel-ide-helper
モデル作成
今回はTeam(チーム)モデルとGymnast(体操選手)モデルの2つを用意します。
モデルの関係性は、『1つのチームには複数の体操選手が所属している』という状態を作りたいと思います。
まずはTeamモデルを作成します。
以下コマンドを実行してください。
% sail artisan make:model -m Team
Teamモデルがmigrationファイルと共に生成されました。
(make:model -m
とすることでmodel用のmigrationファイルを一緒に生成できます。)
今回はTeamがもつフィールドとして、
- ID
- Country
- Created_at
- Updated_at
を用意します。
migrationファイルを以下のように変更します。
2022_08_17_024037_create_teams_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('teams', function (Blueprint $table) {
$table->id();
$table->string('country');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('teams');
}
};
続いて、以下コマンドを実行しGymnastモデルを作成します。
% sail artisan make:model -m Gymnast
Teamと同様、migrationファイルと共にモデルが生成されました。
Gymnastモデルがもつフィールドとして
- name
- age
- team_id
- created_at
- updated_at
を用意します。
migrationファイルを以下のように変更します。
2022_08_17_030359_create_gymnasts_table.php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('gymnasts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->integer('age');
$table->foreignId('team_id')->constrained();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('gymnasts');
}
};
$table->foreignId('team_id)->constrained();
にご注目ください。
こうすることでgymnastsテーブルにteam_idを登録することができ、且つteamsテーブルに存在しているIDしか登録できない外部キー制約も設定されている状態となります。
モデル内でテーブルの関連付けを行うためファイルを変更していきます。
Team.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Team extends Model
{
use HasFactory;
protected $fillable = [
'country'
];
public function gymnasts(){
return $this->hasMany(Gymnast::class);
}
}
1つのチームには複数の体操選手が所属していることを示したいので、
functionの中身は
return $this->hasMany(Gymnast::class);
とします。
一方Gymnastモデルは以下のようになっています。
Gymnast.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Gymnast extends Model
{
use HasFactory;
protected $fillable = [
'name',
'age',
'team_id'
];
public function team(){
return $this->belongsTo(Team::class);
}
}
1人の体操選手は1つのチームに所属していることを示したいので、functionの中身は
return $this->belongsTo(Team::class);
とします。
モデルとmigrationファイルの整備が完了したので、以下コマンドでDBに変更を反映させます。
% sail artisan migrate
テストデータ作成
次にfactoryを使ってテストデータを作成し流し込む作業をします。
以下のコマンドを実行してください。
% sail artisan make:factory TeamFactory –model=Team
% sail artisan make:factory GymnastFactory –model=Gymnast
--modelオプションにより、factoryが生成するモデルの名前を指定できます。
生成された各factoryファイルを以下のように変更してください。
TeamFactory.php
<?php
namespace Database\Factories;
use App\Models\Team;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Team>
*/
class TeamFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
protected $model = Team::class;
public function definition()
{
return [
'country' => $this->faker->country()
];
}
}
GymnastFactory.php
<?php
namespace Database\Factories;
use App\Models\Team;
use App\Models\Gymnast;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Gymnast>
*/
class GymnastFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
protected $model = Gymnast::class;
public function definition()
{
$teamIDs = Team::all()->pluck('id')->toArray();
return [
'name' => $this->faker->name(),
'age' => $this->faker->numberBetween(18, 28),
'team_id' => $this->faker->randomElement($teamIDs)
];
}
}
GymnastFactoryのuse宣言にはTeamも含まれています。
これはGymnastのフィールド内にteam_idが含まれており、Teamモデルも参照しなければいけないためです。他のモデルを干渉しなければuse宣言にはそのモデルを宣言するだけで大丈夫ですが、モデルの関連付けをする際はこういった部分を忘れないようにしたいですね。(自戒)
また、fakerはテストデータを作成するのに非常に便利ですが、name()やrandomElement()の他にも様々な項目のテストデータを作成することができます。
ぜひ調べてみてください。
最後に以下コマンドを実行し、seedをDBに反映させます。
% sail artisan db:seed
GraphQLの導入
LaravelのGraphQLライブラリをインストールします。
% sail composer require rebing/graphql-laravel
ライブラリに関してはこちらをご参照ください。
次に以下コマンドを入力してください。
% sail artisan vendor:publish –provider=”Rebing\GraphQL\GraphQLServerProvider”
これで、config/graphql.phpで使用するGraphQLの設定ファイルが作成されるかと思います。
この設定ファイルに、Type・Query・Mutationなど使用するGraphQLのオブジェクトを記述していきます。
次に各ディレクトリを作成します。
以下のようにGraphQLディレクトリをapp配下に設置し、その下にMutations,Queries,Typesディレクトリをそれぞれ作成してください。
Type作成
先ほど作成したTypes配下に、TeamType.phpとGymnastType.phpをそれぞれ作成してください。
TeamTypeは以下のように記述します。
TeamType.php
<?php
namespace App\GraphQL\Types;
use App\Models\Team;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
class TeamType extends GraphQLType
{
protected $attributes = [
'name' => 'Team',
'model' => Team::class,
'description' => 'チームのタイプ'
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
],
'country' => [
'type' => Type::nonNull(Type::string()),
],
'gymnasts' => [
'type' => Type::listOf(GraphQL::type('Gymnast')),
]
];
}
}
今回はチームにどの体操選手が所属しているか確認したいため、体操選手のリストを返すgymnastsというフィールドを足しています。
次にGymnastTypeを作成していきます。
GymnastType.php
<?php
namespace App\GraphQL\Types;
use App\Models\Gymnast;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Type as GraphQLType;
class GymnastType extends GraphQLType
{
protected $attributes = [
'name' => 'Gymnast',
'model' => Gymnast::class,
'description' => '体操選手のタイプ'
];
public function fields(): array
{
return [
'id' => [
'type' => Type::nonNull(Type::int()),
],
'name' => [
'type' => Type::nonNull(Type::string()),
],
'age' => [
'type' => Type::nonNull(Type::int()),
],
'team' => [
'type' => GraphQL::type('Team'),
]
];
}
}
TeamType同様、取得した体操選手がどこのチームに所属しているか確認したいため、teamというフィールドを足しています。
Query作成
次に実際にデータを問い合わせ取得するQueryを作成します。
先ほど作成したQueriesディレクトリ配下にTeamとGymnastディレクトリを作成し、それぞれに
- Team
TeamQuery.php
TeamsQuery.php - Gymnast
GymnastQuery.php
GymnastsQuery.php
という構成でQueryファイルを作成してください。
TeamQuery.phpでは、チームをIDで指定して取得したいです。
TeamQuery.php
<?php
namespace App\GraphQL\Queries\Team;
use App\Models\Team;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class TeamQuery extends Query
{
protected $attributes = [
'name' => 'team',
'description' => 'チーム取得'
];
public function type(): Type
{
return GraphQL::type('Team');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
],
];
}
public function resolve($root, $args)
{
return Team::findOrFail($args['id']);
}
}
typeメソッドでは、このクエリが返すオブジェクトのtypeを宣言しています。
argsメソッドでは、このクエリが受け取る引数を宣言しています。ここではidのみ指定しています。
resolveメソッドでは、Eloquentを使って実際のオブジェクトを返しています。
APIを叩く前に、config/grapql.php内に叩くAPIのオブジェクトを記述していきます。
現段階では返すオブジェクトのtypeと先ほど定義したTeamQueryを記述します。
今後QueryやMutationを作成する際は、config/graphql.phpにオブジェクトを記述する必要があります。
<?php
// 省略
'schemas' => [
'default' => [
'query' => [
'team' => \App\GraphQL\Queries\Team\TeamQuery::class,
],
'mutation' => [
'createTeam' => \App\GraphQL\Mutations\Team\CreateTeamMutation::class,
'createGymnast' => \App\GraphQL\Mutations\Gymnast\CreateGymnastMutation::class,
],
// The types only available in this schema
'types' => [
'Team' => \App\GraphQL\Types\TeamType::class,
'Gymnast' => \App\GraphQL\Types\GymnastType::class
],
// Laravel HTTP middleware
'middleware' => null,
// Which HTTP methods to support; must be given in UPPERCASE!
'method' => ['GET', 'POST'],
// An array of middlewares, overrides the global ones
'execution_middleware' => null,
],
],
// 省略
きちんとクエリが取得できているか確認してみます。
今回は簡易的なGraphQLクライアントツールとしてGraphiQLを使用します。
GraphiQLはgraphql-laravelをインストールした時点で、すでにgraphql.php内で実装されていますので、すぐに使用することができます。
ローカルサーバーが起動している状態で、 http://localhost/graphiql にアクセスしてください。
Teamを1つ取得してみます。
ジブラルタルのチームが登録されているので、成功です。
一方で、TeamQuery.phpでは、全てのチームを取得したいと思います。
TeamsQuery.php
<?php
namespace App\GraphQL\Queries\Team;
use App\Models\Team;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class TeamsQuery extends Query
{
protected $attributes = [
'name' => 'teams',
'description' => 'チーム一覧取得'
];
public function type(): Type
{
return Type::listOf(GraphQL::type('Team'));
}
public function resolve($root, $args)
{
return Team::all();
}
}
ここでは指定する引数はないため、argsメソッドは必要ありません。
返されるオブジェクトのtypeと、実際に返すresolveメソッドのみ記述します。
次に、config/graphql.phpにオブジェクトを記述します。
<?php
// 省略
'schemas' => [
'default' => [
'query' => [
'team' => \App\GraphQL\Queries\Team\TeamQuery::class,
'teams' => \App\GraphQL\Queries\Team\TeamsQuery::class,
],
'mutation' => [
],
// The types only available in this schema
'types' => [
'Team' => \App\GraphQL\Types\TeamType::class,
'Gymnast' => \App\GraphQL\Types\GymnastType::class
],
// Laravel HTTP middleware
'middleware' => null,
// Which HTTP methods to support; must be given in UPPERCASE!
'method' => ['GET', 'POST'],
// An array of middlewares, overrides the global ones
'execution_middleware' => null,
],
],
// 省略
GraphiQLで確認しましょう。
チームの一覧を取得できました。
このような形でGymnastQueryとGymnastsQueryを作成していきます。
GymnastQuery.php
<?php
namespace App\GraphQL\Queries\Gymnast;
use App\Models\Gymnast;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class GymnastQuery extends Query
{
protected $attributes = [
'name' => 'gymnast',
'description' => '体操選手取得'
];
public function type(): Type
{
return GraphQL::type('Gymnast');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
]
];
}
public function resolve($root, $args)
{
return Gymnast::findOrFail($args['id']);
}
}
GymnastsQuery.php
<?php
namespace App\GraphQL\Queries\Gymnast;
use App\Models\Gymnast;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Query;
class GymnastsQuery extends Query
{
protected $attributes = [
'name' => 'gymnasts',
'description' => '体操選手一覧取得'
];
public function type(): Type
{
return Type::listOf(GraphQL::type('Gymnast'));
}
public function resolve($root, $args)
{
return Gymnast::all();
}
}
config/graph.phpにもQueryの内容を記述します。
<?php
// 省略
'schemas' => [
'default' => [
'query' => [
'team' => \App\GraphQL\Queries\Team\TeamQuery::class,
'teams' => \App\GraphQL\Queries\Team\TeamsQuery::class,
'gymnast' => \App\GraphQL\Queries\Gymnast\GymnastQuery::class,
'gymnasts' => \App\GraphQL\Queries\Gymnast\GymnastsQuery::class,
],
'mutation' => [
],
// The types only available in this schema
'types' => [
'Team' => \App\GraphQL\Types\TeamType::class,
'Gymnast' => \App\GraphQL\Types\GymnastType::class
],
// Laravel HTTP middleware
'middleware' => null,
// Which HTTP methods to support; must be given in UPPERCASE!
'method' => ['GET', 'POST'],
// An array of middlewares, overrides the global ones
'execution_middleware' => null,
],
],
// 省略
GraphiQLで確認します。
サン・マルタン島所属の選手が取得できました。
この場で「本当にサン・マルタン島に体操選手がいるのか」について議論するつもりはありませんが、
ここではGymnastTypeでteamを指定できるようにしているので、体操選手の所属チームまで取得することができます。
体操選手一覧についても同様に取得できています。
各Mutation作成
Team, GymnastでそれぞれMutationを作成していきます。
以下のような構成でMutationファイルを作成してください。
まずはTeamを作成するMutationを作ります。
CreateTeamMutation.php
<?php
namespace App\GraphQL\Mutations\Team;
use App\Models\Team;
use Rebing\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
class CreateTeamMutation extends Mutation
{
protected $attributes = [
'name' => 'createTeam',
'description' => 'チーム作成'
];
public function type(): Type
{
return GraphQL::type('Team');
}
public function args(): array
{
return [
'country' => [
'name' => 'country',
'type' => Type::nonNull(Type::string()),
],
];
}
public function resolve($root, $args)
{
$team = new Team();
$team->fill($args);
$team->save();
return $team;
}
}
typeメソッドで返すtypeを指定し、argsメソッドで引数を指定しています。
resolveメソッドでは、Eloquentを使って実際にTeamが作成される動きを記述しています。
以降の記述は省かせて頂きますが、graphql.php内にMutationのオブジェクトを記述してください。
Mutationがきちんと動くのか、GraphiQLで確認したいと思います。
韓国のチームを作成することに成功しました。
この調子でUpdateとDeleteのMutationも作成していきます。
UpdateTeamMutation.php
<?php
// app/graphql/mutations/category/UpdateCategoryMutation
namespace App\GraphQL\Mutations\Team;
use App\Models\Team;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Mutation;
class UpdateCategoryMutation extends Mutation
{
protected $attributes = [
'name' => 'updateCategory',
'description' => 'チーム更新'
];
public function type(): Type
{
return GraphQL::type('Team');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
],
'country' => [
'name' => 'title',
'type' => Type::nonNull(Type::string()),
],
];
}
public function resolve($root, $args)
{
$team = Team::findOrFail($args['id']);
$team->fill($args);
$team->save();
return $team;
}
}
GraphiQL結果
idが1のTeamのcountryを、アフガニスタンに変更することができました。
DeleteTeamMutation.php
<?php
// app/graphql/mutations/category/UpdateCategoryMutation
namespace App\GraphQL\Mutations\Team;
use App\Models\Team;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Mutation;
class DeleteTeamMutation extends Mutation
{
protected $attributes = [
'name' => 'deleteTeam',
'description' => 'チーム削除'
];
public function type(): Type
{
return Type::boolean();
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::int(),
'rules' => ['required']
],
];
}
public function resolve($root, $args)
{
$team = Team::findOrFail($args['id']);
return $team->delete() ? true : false;
}
}
GraphiQLで確認します。
idが5のTeamを削除することに成功しました。
teamsのQueryを取得して確認します。
idが5のTeamは無事削除できています。
続いてGymnastのMutationを作成していきます。
CreateGymnastMutation.php
<?php
namespace App\GraphQL\Mutations\Gymnast;
use App\Models\Gymnast;
use Rebing\GraphQL\Support\Mutation;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
class CreateGymnastMutation extends Mutation
{
protected $attributes = [
'name' => 'createGymnast',
'description' => '体操選手作成'
];
public function type(): Type
{
return GraphQL::type('Gymnast');
}
public function args(): array
{
return [
'name' => [
'name' => '名前',
'type' => Type::nonNull(Type::string()),
],
'age' => [
'name' => '年齢',
'type' => Type::nonnull(Type::int()),
],
'team_id' => [
'name' => '所属チーム',
'type' => Type::nonNull(Type::int()),
'rules' => ['exists:teams,id']
]
];
}
public function resolve($root, $args)
{
$gymnast = new Gymnast();
$gymnast->fill($args);
$gymnast->save();
return $gymnast;
}
}
基本的な部分はcreateTeamMutationと同様です。
GraphiQLで確認してみます。
メキシコ所属のヤマダタロウ選手が爆誕しました。
UpdateGymnastMutation.php
<?php
namespace App\GraphQL\Mutations\Gymnast;
use App\Models\Gymnast;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Facades\GraphQL;
use Rebing\GraphQL\Support\Mutation;
class UpdateGymnastMutation extends Mutation
{
protected $attributes = [
'name' => 'updateGymnast',
'description' => '体操選手情報更新'
];
public function type(): Type
{
return GraphQL::type('Gymnast');
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
],
'name' => [
'name' => 'name',
'type' => Type::nonNull(Type::string()),
],
'age' => [
'name' => 'age',
'type' => Type::nonNull(Type::int()),
],
'team_id' => [
'name' => 'team_id',
'type' => Type::nonNull(Type::int()),
'rules' => ['exists:teams,id']
]
];
}
public function resolve($root, $args)
{
$gymnast = Gymnast::findOrFail($args['id']);
$gymnast->fill($args);
$gymnast->save();
return $gymnast;
}
}
GraphiQL結果
idが3の体操選手の名前をヤマダシロウに、年齢を19歳に変更することができました。
続いて体操選手を削除するMutationを作成します。
DeleteGymnastMutation.php
<?php
namespace App\GraphQL\Mutations\Gymnast;
use App\Models\Gymnast;
use GraphQL\Type\Definition\Type;
use Rebing\GraphQL\Support\Mutation;
class DeleteGymnastMutation extends Mutation
{
protected $attributes = [
'name' => 'deleteGymnast',
'description' => '体操選手削除'
];
public function type(): Type
{
return Type::boolean();
}
public function args(): array
{
return [
'id' => [
'name' => 'id',
'type' => Type::nonNull(Type::int()),
'rules' => ['exists:gymnasts']
]
];
}
public function resolve($root, $args)
{
$gymnast = Gymnast::findOrFail($args['id']);
return $gymnast->delete() ? true : false;
}
}
GraphiQLで確認します。
先ほど名前と年齢を更新した、ヤマダシロウ選手を削除できました。
最後に
今回はLaravelとGraphQLを使ってチームと体操選手の情報を取得、そして作成・更新・削除するAPIを作りました。
今回は簡単なモデル関係に留めていますが、実際の業務になるとより膨大なテーブル・カラム数、複雑なモデル関係になっているかと思います。
私自身、最近LaravelとGraphQLを触れ始めたので、未だ非常に多くのことが未知の領域です。
しかしこうして記事にすることで理解度は増したのかなと感じています。
今後継続的に技術記事を投稿することで、記事を見て頂けた方はもちろん、私自身の技術力の向上にも寄与することができたらと考えています。
もし誤っている箇所があれば、是非コメントを頂けたら嬉しいです。
それではまた👋
Discussion