スクリーンショット_2019-10-02_1

「Laravel + Vue.jsではじめる 実践 GraphQL入門」の全貌を大公開します!〜GraphQL + Laravelでバックエンドを開発!(アカウント登録機能)編〜

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

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


前回noteから実践編について書いています。
今回は第3弾!
GraphQL + Laravelでバックエンドを開発!(アカウント登録機能)編です。

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


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

アカウント登録機能

それではまずはアカウントの登録機能から実装を始めます。

accountsテーブルの作成
アカウント情報を保持するテーブルをDBに作成していきます。

マイグレーションを作成
"artisan make:migration" を使ってマイグレーションファイルを生成します。

$ php artisan make:migration create_accounts_table

上記コマンドを実行して "database/migrations" 配下に生成されたマイグレーションファイルを次の内容に編集します。

/backend/database/migrations/2019_03_25_052542_create_accounts_table.php

<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAccountsTable extends Migration
{
     /**
      * Run the migrations. *
      * @return void
      */
     public function up()
     {
         Schema::create('accounts', function (Blueprint $table) {
             $table->bigIncrements('id');
             $table->string('twitter_id')->unique();
             $table->string('name');
             $table->string('email')->unique();
             $table->string('avatar')->nullable();
             $table->timestamp('email_verified_at')->nullable();
             $table->string('password');
             $table->rememberToken();
             $table->timestamp('logged_in_at')->nullable();
             $table->timestamp('signed_up_at')->nullable();
             $table->timestamps();
         });
     }

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

マイグレーションファイルが用意できたので、 "artisan migrate" でDBにテーブルを作成します。

$ php artisan migrate


Accountモデルの生成
次のコマンドを実行してAccountモデルを生成します。

$ php artisan make:model Models/Account

モデルファイルは App/Models ディレクトリ配下に配置します。
App/Models ディレクトリ配下に生成したモデルファイルを下記の内容に編集してください。

<?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(ミューテーション)の作成
アカウント登録に使用するGraphQLのMutationを作成します。 次のようにしてMutationを配置するディレクトリとgraphqlファイルを作成してください。

$ mkdir graphql/Mutations
$ touch graphql/Mutations/CreateAccount.graphql

作成した CreateAccount.graphql を下記に変更します。

type Mutation {
   CreateAccount(
     name: String @rules(apply: ["required", "string", "max:40"])
     twitter_id: String @rules(apply: ["required", "string", "max:40"])
     email: String @rules(apply: ["required", "email", "max:255", "unique:accounts,em
ail"])
     password: String @rules(apply: ["required", "string", "min:6", "confirmed"])
        password_confirmation: String @rules(apply: ["required", "string"])
    ): CreatedAccount @field(resolver: "RegisterAccountResolver@resolve") # 1
}

上記のミューテーションでは"@rules""@field"ディレクティブを使用していますが、これらのディレクティブ は "nuwave/lighthouse" で定義されているため利用可能なディレクティブです。

◾️rulesディレクティブ
 rulesディレクティブを設定することで、Laravelのバリデーションルールを使用できるようになります。

◾️fieldディレクティブ
resolverとして使用するクラスとメソッドを指定することができます。
利用可能なディレクティブは他にもありますので、"nuwave/lighthouse" のドキュメントを参照して、どんなデ ィレクティブがあるか見てみると良いでしょう。

①では、RegisterAccountResolverクラスのresolveメソッドを使って CreateAccount を解決します。


Type(タイプ)の作成
CreateAccount
はレスポンスとして CreatedAccount を返却するように定義しています。
次はこの CreatedAccount を作成します。 Typeを格納するためのディレクトリとgraphqlファイルを用意します。

$ mkdir graphql/Types
$ touch graphql/Types/CreatedAccount.graphql

CreatedAccount の実装を下記に示します。

type CreatedAccount {
   account: Account
   token: Token
}

CreatedAccount が返却するタイプは AccountToken です。
同様にそれぞれのタイプを追加します。

$ touch graphql/Types/Account.graphql
$ touch graphql/Types/Token.graphql

backend/graphql/Types/Account.graphql

type Account {
    id: ID
    twitter_id: String
    name: String
    email:String
    avatar: String
}

backend/graphql/Types/Token.graphql

type Token {
    access_token: String
    token_type: String
    expires_in: Int
}


graphqlファイルの登録
作成したタイプやミューテーションは、まだアプリケーションが読み込めるようになっていません。
これらのgraphqlファイルを反映するためには /graphql/schema.graphql からインポートする必要があります。
schema.graphqlの先頭行に下記の import を追加してください。

#import Types/*.graphql <--- この行を追加
#import Mutations/*.graphql <--- この行を追加

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

"A date string with format 'Y-m-d', e.g. '2011-05-23'."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")


リゾルバーの作成
次はCreateAccount.graphqlのresolverとして設定した"RegisterAccountResolver"を作成します。
リゾルバーはartisanコマンドを使ってひな形を生成することができます。

$ php artisan lighthouse:mutation RegisterAccountResolver

コマンドを実行すると、 app/GraphQL/Mutations/RegisterAccountResolver.php が生成されます。
RegisterAccountResolver.php を次のように書き換えます。

<?php
namespace App\GraphQL\Mutations;
use App\Models\Account;
use Carbon\Carbon;
use GraphQL\Type\Definition\ResolveInfo;
use Hash;
use Illuminate\Auth\AuthManager;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Auth\RegistersUsers;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class RegisterAccountResolver
{

   use RegistersUsers;

   /**
    * 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 fields of a single query.
    * @param ResolveInfo $resolveInfo Information about the query itself, such as the execution state, the field name, path to the field from the root, and more.
    *
    * @return mixed
    */
   public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
    {
       event(new Registered($account = $this->create($args))); // ①

    /** @var \Illuminate\Auth\AuthManager $authManager */
    $authManager = app(AuthManager::class);

      /** @var \Tymon\JWTAuth\JWTGuard $guard */
      $guard = $authManager->guard('api');
      $token = $guard->login($account);

      return [
          'account' => $account,
          'token' => [
              'access_token' => $token,
              'token_type'   => 'bearer',
              'expires_in'   => $guard->factory()->getTTL() * 60,
          ],
       ];
    }
  
    /**
     * Create a new user instance after a valid registration.
     *
     * @param array $data
     * @return \App\Models\Account
     */
    protected function create(array $data)
    {
        return Account::create([
            'name'         => $data['name'],
            'twitter_id'   => $data['twitter_id'],
            'email'        => $data['email'],
            'password'     => Hash::make($data['password']),
            'logged_in_at' => Carbon::now(),
            'signed_up_at' => Carbon::now(),
        ]); 
    }

}

リクエストに含まれる入力値は"resolve()"の引数の"$args"に格納されています。
そのため①では"$args""create()"に渡し、必要な値を取得します。
認証にはJWTを使用するため、戻り値で生成したトークンを返却しています。


認証ロジックの設定変更
JWTを使用するために認証で使用するロジックを変更します。
/config/auth.php を次に示す内容に変更します。
※auth.phpに元々記載されているコメントは省略しています。

<?php

return [

    'defaults' => [
        'guard' => 'api', // ①
        'passwords' => 'accounts', // ②
     ],

     'guards' => [
         'web' => [
             'driver' => 'session',
             'provider' => 'users',
         ],

         'api' => [
             'driver' => 'jwt', // ③
             'provider' => 'accounts', // ④
             'hash' => false,
         ], 
     ],

     'providers' => [
         'users' => [
             'driver' => 'eloquent',
             'model' => App\User::class,
          ],

          'accounts' => [ // ⑤
              'driver' => 'eloquent',
              'model' => App\Models\Account::class,
          ]
     ],

     'passwords' => [
         'users' => [
             'provider' => 'users',
             'table' => 'password_resets',
             'expire' => 60,
          ],

        'accounts' => [ // ⑥
              'provider' => 'accounts', // ⑦
           'table' => 'password_resets',
           'expire' => 60,
        ],
    ],

];

①、②ではデフォルトで使用する"guard""passwords"を変更しています。
①で指定した"api"guardの設定を③と④で作成します。
JWT認証を使用するため、apiのドライバーを3で"jwt"に指定しています。
④ではプロバイダーを"accounts"に変えています。
④で設定したプロバイダーの内容を⑤で追加します。モデルには今回Accountsモデルを使用します。
②の"passwords"に対応する設定が⑥、⑦の内容です。


authミドルウェアを作成する
“app/Http/Middleware/Authenticate.php”では下記①のように”\App\Http\Middleware\Authenticate::class”が設定されています。

class Kernel extends HttpKernel
{
   // ...

   protected $routeMiddleware = [
         'auth' => \App\Http\Middleware\Authenticate::class, // ①
         'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,

         // ...
     ];

ただ、このAuthenticate::classは認証失敗時のリダイレクト先がログインページになっています。
どのページにリダイレクトさせるかのハンドリングはVue.js側で行うので、カスタムミドルウェアを使って、ログインページにリダイレクトしないように変更します。
次のコマンドでミドルウェアを生成します。

$ php artisan make:middleware GraphQLAuthenticate

GraphQLAuthenticateの実装を次に示します。

<?php

namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate;

class GraphQLAuthenticate extends Authenticate
}
   /**
    * Get the path the user should be redirected to when they are not authenticated.
    *
    * @param \Illuminate\Http\Request $request
    * @return string
    */
   protected function redirectTo($request)
   {
       if (!$request->expectsJson()) {
           return false; // ①
       } 
   }

}

“Authenticate.php"で”route('login')”をリターンしていた処理を”false”を返却するようにしています。
あとは“app/Http/Kernel.php”の”auth”で設定している箇所を”GraphQLAuthenticate::class”に変えればOKです(①)。

class Kernel extends HttpKernel
{
    // ...

    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\GraphQLAuthenticate::class, // ①
     'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,

     // ...
    ];


アカウントを登録する
ここまででアカウントを登録する準備が整いました。 GraphQL Playgroundを使ってアカウントの登録をします。
"artisan serv"を実行してローカルサーバを起動し、 http://localhost:8000/graphql-playground にアクセスしてください。
Playgroundの左側の入力欄に下記の内容を入力してください。

mutation { # ①
  CreateAccount( # ②
    name: "foo" # ③
    twitter_id: "foo1"
    email: "foo1@example.com"
    password: "xxxxxx"
    password_confirmation: "xxxxxx"
  ) {
    account { # ④
      twitter_id
    }
    token {
      access_token
    }
  }
}

アカウント登録処理はミューテーションのため、 mutation を宣言し(①)、使用するミューテーションを指 定します(②)。
CreateAccountの実行に必要な引数を設定し(③)、レスポンスとして取得したい項目を④のように記述しま す。
Playgroundの実行結果は右側にレスポンスが表示されます。

{
   "data": {
     "CreateAccount": {
       "account": {
         "twitter_id": "foo1"
       },
       "token": {
         "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9sb2
NhbGhvc3Q6ODAwMFwvZ3JhcGhxbCIsImlhdCI6MTU1MzQ5NzQ3NiwiZXhwIjoxNTUzNTAxMDc2LCJuYmYiOjE1NT M0OTc0NzYsImp0aSI6Ik82SGhkQ24wWEFvbTgwa20iLCJzdWIiOjEsInBydiI6ImM4ZWUxZmM4OWU3NzVlYzRjNz M4NjY3ZTViZTE3YTU5MGI2ZDQwZmMifQ.YjH6t805T4DJQZqTS-YZ1cprMKcgk525t_Gpjk8Fsbs"
       }
     }
   }
 }


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

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

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


Fin.

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

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

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


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