見出し画像

「Laravel + Vue.jsではじめる 実践 GraphQL入門」の全貌を大公開します!〜GraphQL + Laravelでバックエンドを開発!(ログイン機能・ツイート機能)編〜

こんにちは。kzkohashi です。
FISM という会社でCTOをやっております。

今年4月に「Laravel + Vue.jsではじめる 実践 GraphQL入門」という技術書籍を出版しました。​


前々回から実践編について引き続き書いています。
今回は第4弾!
GraphQL + Laravelでバックエンドを開発!(ログイン機能・ツイート機能)編です。

※今回もコードがメインになります。どうぞ!!


✂︎ ---------------------

ログイン機能

作成済みのアカウント情報を使ってログインする機能を実装します。

ログイン用のMutation(ミューテーション)の作成
ログインで使用するミューテーションを作成します。

$ touch graphql/Mutations/Login.graphql

上記のコマンドを実行してファイルを作成し、次の内容を設定してください。

extend type Mutation { # ①
    Login( # ②
        email: String @rules(apply: ["required", "email"])
        password: String @rules(apply: ["required", "max:16"])
    ): Token @field(resolver: "LoginResolver@resolve") # ③
}

2つ目以降のMutationを定義する際には extend を設定します(①)。
ログインにはメールアドレスとパスワードを使用します。(②)
Loginミューテーションはリゾルバーとして LoginResolver を使用し、 Token タイプを返却します(③)。


ログインリゾルバーの作成
それではartisanコマンドを使って LoginResolver を生成しましょう。

$ php artisan lighthouse:mutation LoginResolver

ログインリゾルバーの実装は下記の通りです。

<?php

namespace App\GraphQL\Mutations;

use Carbon\Carbon;
use GraphQL\Type\Definition\ResolveInfo;
use Illuminate\Auth\AuthManager;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class LoginResolver
{

    /**
     * Return a value for the field.
     *
     * @param null $rootValue Usually contains the result returned from the parent field. In this case, it is always `null`.
     * @param array $args The arguments that were passed into the field.
     * @param GraphQLContext|null $context Arbitrary data that is shared between all fie lds of a single query.
     * @param ResolveInfo $resolveInfo Information about the query itself, such as the e xecution state, the field name, path to the field from the root, and more.
     *
     * @return mixed
     */
     public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInf o $resolveInfo)
     {
         $authManager = app(AuthManager::class);

         /** @var \Tymon\JWTAuth\JWTGuard $guard */
         $guard = $authManager->guard('api');
         $token = $guard->attempt([
             'email' => $args['email'],
             'password' => $args['password'],
         ]);

         if ($token) {
             /** @var \App\Models\Account $account */
             $account = auth()->user();
             $account->logged_in_at = Carbon::now();
             $account->save();
         }

         return [
             'access_token' => $token,
             'token_type' => 'bearer',
             'expires_in' => $guard->factory()->getTTL() * 60,
         ];
     }
} 


作成したアカウントでログインする
ログイン処理が用意できたので、Playgroundを使って動作を確認します。
リクエストを入力して実行してみましょう。

mutation {
  Login(
    email: "foo1@example.com"
    password: "xxxxxx"
  ) {
    access_token token_type expires_in
  }
}

ログインに成功すると次のようなレスポンスが返却されます。

{
  "data": {
    "Login": {
      "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2Nh
bGhvc3Q6ODAwMFwvZ3JhcGhxbCIsImlhdCI6MTU1MzQ5ODE2MiwiZXhwIjoxNTUzNTAxNzYyLCJuYmYiOjE1NTM0 OTgxNjIsImp0aSI6IjlkS0diQ0p6Zm1vd2lsYzUiLCJzdWIiOjEsInBydiI6ImM4ZWUxZmM4OWU3NzVlYzRjNzM4 NjY3ZTViZTE3YTU5MGI2ZDQwZmMifQ.gjoN9cIWkH_OW80pUGzyvpx0TVzcYh_ORXDkNYCqVbY",
      "token_type": "bearer",
      "expires_in": 3600 }
    }
  }
}

レスポンスで取得した access_token はタイムラインの表示やツイート機能などの、ログイン状態で実行する処理で使用します。


ツイート機能

アカウント登録とログインができるようになったので、本節ではツイート機能を実装を説明します。


tweetsテーブル、timelinesテーブルのマイグレーションを作成
ツイート時に必要となるtweetsテーブルとtimelinesテーブルをDBに用意します。
次のコマンドを実行してマイグレーションファイルを生成してください。

$ php artisan make:migration create_tweets_table
$ php artisan make:migration create_timelines_table

マイグレーションファイルを次の内容に編集します。
◾️/backend/database/migrations/2019_03_25_071844_create_tweets_table.php

<?php

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

class CreateTweetsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tweets', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedInteger('account_id');
            $table->string('content', 140);
            $table->unsignedInteger('reply_id')->nullable();
            $table->timestamp('tweeted_at'); $table->softDeletes();
            $table->timestamps();
        });
    }

   /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
        Schema::dropIfExists('tweets');
    }
}

◾️/backend/database/migrations/2019_03_25_071911_create_timelines_table.php

<?php

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

class CreateTimelinesTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
       Schema::create('timelines', function (Blueprint $table) {
           $table->bigIncrements('id');
           $table->unsignedInteger('account_id');
           $table->unsignedInteger('tweet_id');
           $table->unsignedInteger('favorite_id')->nullable();
           $table->unsignedInteger('original_favorite_id')->nullable();
           $table->softDeletes();
           $table->timestamps();
       });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('timelines');
    }
}


マイグレーションの実行
マイグレーションを実行してDBにテーブルが作成されているかどうかを確認してください。

$ php artisan migrate


Tweet、Timelineモデルの作成
テーブルが用意できたところで、モデルを作成します。 モデルは次のコマンドで作成できます。

$ php artisan make:model Models/Tweet
$ php artisan make:model Models/Timeline


Tweetモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Tweet extends Model
{
    use SoftDeletes; protected $guarded = [];

    public function account()
    {
        return $this->belongsTo(Account::class);
    }
}

Timelineモデル

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Timeline extends Model
{
    use SoftDeletes; protected $guarded = [];

    public function tweet()
    {
        return $this->belongsTo(Tweet::class);
    }
}
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class Account extends Authenticatable implements JWTSubject
{

    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name',
        'twitter_id',
        'email',
        'password',
        'logged_in_at',
        'signed_up_at',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

   /**
    * Get the identifier that will be stored in the subject claim of the JWT.
    *
    * @return mixed
    */
   public function getJWTIdentifier()
   {
       return $this->getKey();
   }

   /**
    * Return a key value array, containing any custom claims to be added to the JWT.
    *
    * @return array
    */
   public function getJWTCustomClaims()
   {
       return [];
   }

}


ツイート用のMutation(ミューテーション)の作成
これまでと同様にツイート用のミューテーションを作成します。

$ touch graphql/Mutations/CreateTweet.graphql
extend type Mutation @group(middleware: ["auth:api"]) { # ①
    CreateTweet(
        content: String @rules(apply: ["required", "string", "max:140"])
    ): Tweet @field(resolver: "CreateTweetResolver@resolve") # ②
}

ツイートはログイン認証済みの場合のみできるように auth:api ミドルウェアを設定します(①)。
リゾルバーには CreateTweetResolver を用いて解決し、レスポンスはTweetタイプを返します(②)。


ツイート用のTypeの作成
ツイート時のレスポンスとして返却するTweetタイプを定義します。

  $ touch graphql/Types/Tweet.graphql
type Tweet {
    id: ID!
    account_id: Int!
    content: String!
    reply_id: Int
    tweeted_at: DateTime!
    account: Account!
}


ツイート用のリゾルバーの作成
ツイート処理を行うカスタムリゾルバーを作成します。

$ php artisan lighthouse:mutation CreateTweetResolver
<?php

namespace App\GraphQL\Mutations;

use App\Models\Account;
use App\Models\Timeline;
use App\Models\Tweet;
use Carbon\Carbon;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class CreateTweetResolver
{

    /**
     * Return a value for the field. 
     *
     * @param null $rootValue Usually contains the result returned from the parent field
. In this case, it is always `null`.
     * @param array $args The arguments that were passed into the field.
     * @param GraphQLContext|null $context Arbitrary data that is shared between all fie
lds of a single query.
     * @param ResolveInfo $resolveInfo Information about the query itself, such as the e
xecution state, the field name, path to the field from the root, and more.
     *
     * @return mixed
     */
    public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInf
o $resolveInfo)
    {
       /** @var \App\Models\Account $account */
       $account = auth()->user();

       $tweet = $this->createTweet($account, $args); // ①

       $this->addTweetToTimeline($account, $tweet); // ②

       return $tweet;
    }

    /**
     * @param \App\Models\Account $account
     * @param array $data
     * @return \App\Models\Tweet
     */
    protected function createTweet(Account $account, array $data)
    {
        return Tweet::create([
            'account_id' => $account->id,
            'content' => $data['content'],
            'tweeted_at' => Carbon::now(),
        ]);
    }

    /**
     * @param \App\Models\Account $account
     * @param \App\Models\Tweet $tweet
     * @return \App\Models\Timeline
     */
    protected function addTweetToTimeline(Account $account, Tweet $tweet)
    {
        return Timeline::create([
            'account_id' => $account->id,
            'tweet_id' => $tweet->id,
        ]);
    }

 }

ツイート時の処理は、ツイート内容をtweetテーブルに登録(①)した後に、そのツイートをタイムラインに登録(②)します。


ツイートしてみる
ツイートができるかどうか、GraphQL Playgroundを用いて試してみましょう。

mutation {
  CreateTweet(content: "技術書展6") {
    id
    content
  }
}

HTTP HEADERS を設定していることを確認した上で上記のミューテーションを実行してください。

{
  "data": {
    "CreateTweet": {
      "id": "1",
      "content": "技術書展6"
    }
  }
}

レスポンスとtweets、timelinesテーブルの登録結果を確認し、期待通りにデータが登録されていることを確認してください。


✂︎ ---------------------

いかがでしたでしょうか?
ここまでお読みいただき、ありがとうございます!

次回も来週木曜日に続編を公開します!
引き続きご覧くださいませ。


Fin.

▼ Twitterもやってます。よければフォローもお願いします🙇🏿‍♂️

▼ FISM社についてはこちら💁🏿‍♂️

▼ 現在Wantedlyにて開発メンバー募集中です!GraphQL + Laravel + Vue.js + Swift で開発しております👨🏿‍💻まずはお気軽にお話ししましょう🙋🏿‍♂️


この記事が気に入ったらサポートをしてみませんか?