「Laravel + Vue.jsではじめる 実践 GraphQL入門」の全貌を大公開します!〜GraphQL + Laravelでバックエンドを開発!(タイムライン表示機能〜フォロー機能)編〜
見出し画像

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

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

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


前回noteから実践編について書いています。
今回は第3弾!
GraphQL + Laravelでバックエンドを開発!(タイムライン表示機能〜フォロー機能)編です。


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

タイムライン表示機能

ツイートしてタイムラインへの追加ができるようになったので、次はタイムライン情報の取得処理を実装します。

タイムライン表示用のQuery(クエリ)の作成
本節で実装する機能は登録や更新などのデータの変更を行わない、データを取得する処理となるため、Query(クエリ)に分類されます。
そのためクエリで使用する新たにディレクトリを用意し、クエリ用のgraphqlを追加するようにします。

$ mkdir graphql/Queries
$ touch graphql/Queries/Timeline.graphql

Timeline.graphql は次に示す内容にしておきます。

extend type Query @group(middleware: ["auth:api"]) { # ①
    Timeline(id: Int!) : [Timeline] @field(resolver: "TimelineResolver@resolve") # ②
}

timelinesテーブルはtweetsテーブルとリレーションするようになっています。
type Query は既にschema.graphqlで1つ設定されているため、ここでは extend を宣言しています(①)。

タイムラインの取得処理では、複数のTimelineを返却します。
複数のデータを返却する場合、タイプを [] で囲む必要があるため、ここでは [Timeline] を設定しています(②)。

入力値に id: Int! を定義していますが、このidは timelines テーブルの id を指します。



schema.graphqlでQueriesをインポートする
graphql/Queries
ディレクトリ配下のgraphqlを読み込むようにするために、graphql/schema.graphql にimport処理を追記します。

#import Types/*.graphql
#import Mutations/*.graphql#import Queries/*.graphql



Timelineタイプの作成
レスポンスで返却するTimelineタイプを作成します。

$ touch graphql/Types/Timeline.graphql
type Timeline { 
  id: ID!
   account_id: Int!
    tweet_id: Int!
    favorite_id: Int
    tweet: Tweet
}


タイムラインリゾルバーの作成
続いてタイムラインを取得するためのリゾルバーを作成します。

$ php artisan lighthouse:query TimelineResolver
<?php
namespace App\GraphQL\Queries;

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

class TimelineResolver
{

    /**
     * Return a value for the field. 
     *
     * @param null $rootValue Usually contains the result returned from the parent field 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)
    {
        $query = Timeline
            ::with([ 'tweet',
            ])
            ->where('account_id', auth()->user()->id);

         if ($args['id']) {
             $query->where('id', '<', $args['id']); // ①
    }

    $timelines = $query->orderByDesc('id')
               ->limit(10) // ②
               ->get();
        return $timelines;
    }
} 


リクエストに含まれる timelines.id を使用して(①)、その後のタイムラインデータを10件取得します (②)。


タイムラインを表示する
GraphQL Playgroundの HTTP HEADERS を設定しているのを確認し、クエリを実行してみます。

query { # ①
  Timeline(id: 0) { # ②
    id
    tweet {
     id
     content
    }
  } 
}

クエリを実行する際は最初に query を宣言します(①)が、次に示すように省略することが可能です。 id: 0 を指定しているのは指定したidより後のデータを10件取得するようになっているためです。

{
  Timeline(id: 0) {
    id 
    tweet {
      id
      content
    }
  } 
}

成功すると下記例のようなJSONが返却されます。

{
  "data": {
    "Timeline": [
      {
        "id": "1",
        "tweet": {
          "id": "1",
          "content": "技術書展6"
       }
     } 
   ]
 } 
}


フォロー機能

本節では他のアカウントをフォローする機能を追加します。

テーブルの用意
フォロー機能の実装のために、 follows と followers の2つのテーブルを作成する必要があります。 マイグレーションファイルを生成して、DBにテーブルを用意しましょう。

$ php artisan make:migration create_follows_table
$ php artisan make:migration create_followers_table

database/migrations/2019_03_25_081239_create_follows_table.php

<?php

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

class CreateFollowsTable extends Migration
{

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {

        Schema::create('follows', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedInteger('account_id');
            $table->unsignedInteger('follow_account_id');
            $table->timestamps();
        });
    }

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


• database/migrations/2019_03_25_081243_create_followers_table.php


<?php

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

class CreateFollowersTable extends Migration
{

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

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


マイグレーションの実行
migrateコマンドを実行してテーブルを作成します。

$ php artisan migrate


Follow、Followersモデルの作成
テーブルが準備できたので、対応するモデルを作成します。

$ php artisan make:model Models/Follow
$ php artisan make:model Models/Follower
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
class Follow extends Model
{

    protected $guarded = [];
}
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Follower extends Model
{

    protected $guarded = [];

     /**
      * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
      */
     public function account()
     {
         return $this->belongsTo(Account::class); // ①
     }
}

FollowerにはAccountに対してリレーションを持たせます(①)。


アカウントフォロー用のMutation(ミューテーション)の作成

$ touch graphql/Mutations/FollowAccount.graphql
extend type Mutation @group(middleware: ["auth:api"]) { # ①
   FollowAccount(
         id: Int! @rules(apply: ["required", "integer"])
     ): Follow @field(resolver: "FollowAccountResolver@resolve") # ①
}

フォロー機能はログイン認証済みの場合のみできるようにします。
lighthouseではgroupディレティブでLaravelのミドルウェアを指定できるため、 auth:api ミドルウェアを設 定します(①)。
リゾルバーに FollowAccountResolver を使用して解決し、その結果 Follow タイプを返却します(②)。 以降ではその実装について説明します。

Followタイプの作成
まずはFollowタイプを作成します。

$ touch graphql/Types/Follow.graphql
type Follow {
    id: ID!
    account_id: Int!
    follow_account_id: Int!
}


Followerタイプの作成
続いてFollowerタイプも作成します。

$ touch graphql/Types/Follower.graphql
type Follower {
    id: ID!
    account_id: Int!
    follower_account_id: Int!
}


フォロー用のリゾルバーの作成
次はミューテーションのリゾルバーを作成します。

$ php artisan lighthouse:mutation FollowAccountResolver
<?php

namespace App\GraphQL\Mutations;

use App\Models\Account;
use App\Models\Follow;
use App\Models\Follower;
use GraphQL\Type\Definition\ResolveInfo;
use Nuwave\Lighthouse\Support\Contracts\GraphQLContext;

class FollowAccountResolver
{

    /**
     * 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)
    {

        /** @var \App\Models\Account $account */
        $account = auth()->user();

        $follow = $this->followAccount($account, $args); // ①

        $this->addToFollowers($account, $args); // ②

     return $follow;
   }

    /**
     * @param \App\Models\Account $account
     * @param array $data
     * @return \App\Models\Follow
     */
    protected function followAccount(Account $account, array $data)
    {
        return Follow::create([
            'account_id' => $account->id,
            'follow_account_id' => $data['id'],
        ]);
    }
}

followsテーブルのaccount_idにはログイン中のアカウントIDを、follow_account_idにはフォロー対象のアカウ ントIDを登録します(①)。

そしてfollowersテーブルには逆にaccount_idにフォロー対象のアカウントIDを、follower_account_idにログイ ン中のアカウントIDを登録します(②)。

また、今回は処理の簡略化のためにトランザクション制御を使っていませんが、データの更新に失敗した際のハンドリング処理を加えるのも良いですね。


既存処理の変更
CreateTweetResolverにフォロワーのタイムラインへのツイート追加処理を加える

     public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
     {

         /** @var \App\Models\Account $account */
         $account = auth()->user();

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

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

+        $this->addTweetToFollowersTimeline($account, $tweet); // ①

         return $tweet;
 
     }

+     /**
+      * @param \App\Models\Account $account
+      * @param \App\Models\Tweet $tweet
+      */
+     protected function addTweetToFollowersTimeline(Account $account, Tweet $tweet) // ②
+     {
+         foreach ($account->followers as $follower) {
+             Timeline::create([
+                 'account_id' => $follower->follower_account_id,
+                 'tweet_id' => $tweet->id,
+             ]);
+         }
+     }

ツイートをフォロワーのタイムラインに表示するために、12の処理を追加します。


AccountモデルからFollowerモデルへのリレーションを追加

class Account extends Authenticatable implements JWTSubject
{

    // ・ ・ ・

+   /**
+    * @return \Illuminate\Database\Eloquent\Relations\HasMany
+    */
+   public function followers()
+   {
+       return $this->hasMany(Follower::class);
+   }

+   /**
+    * @return \Illuminate\Database\Eloquent\Relations\HasOne */
+    */
+   public function follower()
+   {
+       return $this->hasOne(Follower::class)
+                   ->where('follower_account_id', auth()->user()->id); +}
+   }

}


Accountのスキーマにフォロワー情報を追加

type Account {
    id: ID
    twitter_id: String
    name: String
    email: String
    avatar: String
+ follower: Follower # ①
+ followers: [Follower] # ②
 }

フォロワー情報を返却できるようにするために、アカウントのスキーマにfollowerとfollowersを追加します (①,②)。


他のアカウントをフォローしてみる

それではフォロー機能が実装できたかどうかを確認しましょう。
複数アカウントを登録しておく必要があるため、フォローするアカウントとフォローされるアカウントを、アウント登録機能を使って用意してください。
他のアカウントをフォローするアカウントがどれになるかはGraphQL Playgroundで設定する HTTP HEADERS のトークンによって変わります。
アカウントのフォローで送信するリクエストサンプルを下記に記載します。

mutation {
  FollowAccount(id: 2) { # ①
    id
    account_id
    follow_account_id
  }
}

入力値のidは accounts.id を指定します(①)。
フォロー機能が正常に動作している場合のレスポンスサンプルは次のとおりです。

{
  "data": {
    "FollowAccount": {
      "id": "1",
      "account_id": 1,
      "follow_account_id": 2
    } 
  }
}


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

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

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


Fin.


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

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

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


この記事が気に入ったら、サポートをしてみませんか?
気軽にクリエイターの支援と、記事のオススメができます!
うれしい・・・圧倒的感謝🙇🏿‍♂️
FISM株式会社 CTO, Influencer Marketing. Laravel/Python/自然言語処理/Go. 技術書典6にて「Laravel + Vue.jsではじめる 実践 GraphQL入門」を出しました。2児のパパ。