見出し画像

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

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

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


今回は第6弾!
「GraphQL + Laravelでバックエンドを開発!(フォロー解除機能・いいね機能・いいね解除)編〜」です。

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


フォロー解除機能

本節ではフォローの解除機能を実装します。

フォロー解除用のMutation(ミューテーション)の作成

フォロー解除機能に必要なテーブルは、フォロー機能の実装時に既に作成していますので、早速ミューテーションから作成していきます。

$ touch graphql/Mutations/UnFollowAccount.graphql
extend type Mutation @group(middleware: ["auth:api"]) {
    UnFollowAccount( # ①
        id: Int! @rules(apply: ["required", "integer", "exists:accounts,id"]) # ②
    ): Boolean @field(resolver: "UnFollowAccountResolver@resolve") # ③
}

ミューテーションの名前は UnFollowAccount とします(①)。
入力値としてid(accounts.id)を設定しています(②)。
リゾルバーには UnFollowAccountResolver を使用し、結果をBooleanで返却します。
Booleanは元々用意されているタイプであるため、作成する必要はありません。


フォロー解除用リゾルバーの作成

lighthouse:mutation を使って UnFollowAccountResolver を生成します。

$ php artisan lighthouse:mutation UnFollowAccountResolver

生成した UnFollowAccountResolver を次のように編集します。

<?php

namespace App\GraphQL\Mutations;

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

class UnFollowAccountResolver
{

   /**
    * 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
    * @throws \Exception
    */
   public function resolve($rootValue, array $args, GraphQLContext $context, ResolveInfo $resolveInfo)
   {

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

      if (!$this->unFollowAccount($account, $args)) { // ①
          return false;
      }

      if (!$this->removeFromFollowers($account, $args)) { // ②
          return false;

      }

      return true;

   }

   /**
    * @param \App\Models\Account $account
    * @param array $data
    * @return bool|null
    * @throws \Exception
    */
   protected function unFollowAccount(\App\Models\Account $account, array $data)
   {
       return Follow::where([
           'account_id' =>  $data['id'],
           'follow_account_id' => $account->id,
       ])->delete();
   }
}

フォローを解除する際はfollowsとfollowersテーブルからデータを削除します(①、②)。


フォローを解除してみる

フォロー解除機能が実装できたので、これもGraphQL Playgroundで動作を確認してみます。 HTTP HEADERS でトークンを送信するように設定した上で、リクエストを送信します。
リクエストのサンプルは下記を参照ください。

mutation {
  UnFollowAccount(id: 2) # ①
}

入力値のidは accounts.id を指定します(①)。 実行結果はBooleanが返ってきます。

{
  "data": {
  "UnFollowAccount": true
  }
}



いいね機能

この節ではタイムラインに表示されるツイートをいいねする機能を実装します。

テーブルの用意
本アプリケーションではツイートをいいねをした際、 favorites テーブルにデータを登録します。 favorites テーブルをDBに用意するために、まずはマイグレーションファイルを準備します。

$ php artisan make:migration create_favorites_table

• database/migrations/2019_03_25_094225_create_favorites_table.php

<?php

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


class CreateFavoritesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('favorites', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedInteger('account_id'); // ①
            $table->unsignedInteger('tweet_id'); // ②
            $table->timestamp('favorite_at');
            $table->timestamps();
     });
    }

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

favoritesaccountstweets にリレーションするため、それぞれのidを保持します(①、②)。


マイグレーションの実行

マイグレーションファイルが準備できたところで、マイグレーションを実行してDBにテーブルが作成されてい るかどうかを確認してください。

$ php artisan migrate


Favoriteモデルの作成

Favoriteモデルを作成します。

$ php artisan make:model Models/Favorite
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Favorite extends Model
{

   protected $dates = ['favorite_at']; // ①

   protected $guarded = [];

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

favorites.favorite_at created_atupdated_at と同様に Carbon で扱うために、 $dates に追 加します(①)。
また、Favoriteモデルからaccountsへのリレーションを設定しておきます(②)。

参考)


いいね用のMutation(ミューテーション)の作成

次はいいね機能で使用するミューテーションを作成します。

$ touch graphql/Mutations/MarkFavorite.graphql
extend type Mutation @group(middleware: ["auth:api"]) {
   MarkFavorite( # ①
       tweet_id: Int @rules(apply: ["required", "exists:tweets,id"])
       timeline_id: Int @rules(apply: ["required", "exists:timelines,id"])
   ): Boolean @field(resolver: "MarkFavoriteResolver@resolve") # ②
}

ミューテーションの命名は MarkFavorite とし(①)、その解決は MarkFavoriteResolver で行います (②)。


Favoriteタイプの作成とTimelineタイプの更新

タイムラインを取得するときに、Favoriteの情報も返却できるようにしておきます。
そのためにまずはFavorite用のタイプを作成し、下記のように編集します。

$ touch graphql/Types/Favorite.graphql
type Favorite {
    id: ID!
    account_id: Int!
    tweet_id: Int!
    favorite_at: DateTime!
    account: Account!
}

タイムライン用のタイプも更新します。

type Timeline {
    id: ID!
    account_id: Int!
    tweet_id: Int!
    favorite_id: Int
    tweet: Tweet
    favorite: Favorite # この行を追加
    originalFavorite: Favorite # この行を追加
}

Timelineタイプに追加した“favorite”と”originalFavorite”のリレーションが、まだ”app/Models/Timeline.php”に設定できていないので追加します。

class Timeline extends Model
{

   // ...

   public function favorite()
   {
       return $this->belongsTo(Favorite::class);
   }

   public function originalFavorite()
   {
       return $this->belongsTo(Favorite::class, 'original_favorite_id');
   }
}


いいね用のリゾルバーの作成

MarkFavorite ミューテーションを解決するリゾルバーをコマンドを使って生成します。

$ php artisan lighthouse:mutation MarkFavoriteResolver

リゾルバーの実装を下記に示します。

<?php

namespace App\GraphQL\Mutations;

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

class MarkFavoriteResolver
{

    /**
     * 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 = null, ResolveInfo $resolveInfo)
    {
        /** @var \App\Models\Account $account */
        $account = auth()->user();

        $favorite = null;

        /** @var Favorite $favorite */
        $favorite = $this->createFavorite($account, $args['tweet_id']); // ①
        $this->updateTimelineFavoriteId($account, $args['timeline_id'], $favorite->id); // ②
        $this->addTweetToFollowersTimeline($account, $args['tweet_id'], $favorite->id); // ③

        return !!$favorite; // ④
    }

    protected function createFavorite(Account $account, $tweetId)
    {
        return Favorite::create([
            'account_id' => $account->id,
            'tweet_id' => $tweetId,
            'favorite_at' => Carbon::now()
        ]); 
    }

    protected function updateTimelineFavoriteId(Account $account, $timelineId, $favorite Id)
    {
         return Timeline::where([
             'id' => $timelineId,
             'account_id' => $account->id,
         ])->update(['favorite_id' => $favoriteId]);
    }

         protected function addTweetToFollowersTimeline(Account $account, $tweetId, $favo riteId)
    {
         foreach ($account->followers as $follower) {
             Timeline::create([
                 'account_id' => $follower->follower_account_id,
                 'tweet_id' => $tweetId,
                 'original_favorite_id' => $favoriteId
             ]); 
         }
    } 
}

タイムラインに表示しているツイートを「いいね」した際は、まずfavoritesにデータを追加します(①)。
同時にログイン中のアカウントのタイムライン情報を更新し(②)、更にフォロワーのタイムラインにいいねしたツイートを登録します(③)。


いいね機能を使ってみる
タイムラインに登録されているツイートを「いいね」してみます。
リクエストとレスポンスは、それぞれ下記のようになります。

• リクエスト

mutation {
  MarkFavorite (
    timeline_id: 6
    tweet_id: 5
  )
}

• レスポンス

{
  "data": {
    "MarkFavorite": true
  }
}



いいね解除機能

本節ではいいねを解除する機能を実装します。

いいね解除用のMutation(ミューテーション)の作成

いいねを解除する際に使用するミューテーションを作成します。

$ touch graphql/Mutations/UnMarkFavorite.graphql
extend type Mutation @group(middleware: ["auth:api"]) {
    UnMarkFavorite( # ①
        tweet_id: Int @rules(apply: ["required", "exists:tweets,id"])
        timeline_id: Int @rules(apply: ["required", "exists:timelines,id"])
    ): Boolean @field(resolver: "UnMarkFavoriteResolver@resolve") # ②
}

UnMarkFavorite (①)は UnMarkFavoriteResolver リゾルバーに使用し、結果をBooleanで返却します (②)。


いいね解除用のリゾルバーの作成

次は UnMarkFavoriteResolver を作成します。

$ php artisan lighthouse:mutation UnMarkFavoriteResolver
<?php

namespace App\GraphQL\Mutations;

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

class UnMarkFavoriteResolver
{

    /**
     * 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
     * @throws \Exception
     */
    public function resolve($rootValue, array $args, GraphQLContext $context = null, Res olveInfo $resolveInfo)
    {
        /** @var \App\Models\Account $account */
        $account = auth()->user();

        $this->updateTimelineFavoriteId($account, $args['timeline_id']); // ①

        /** @var Favorite $favorite */
        return $this->deleteFavorite($account, $args['tweet_id']); // ②
     }

     /**
      * @param Account $account
      * @param $timelineId
      * @return bool
      */
     protected function updateTimelineFavoriteId(Account $account, $timelineId)
     {
         return Timeline::where([
             'id' => $timelineId,
             'account_id' => $account->id,
         ])->update(['favorite_id' => null]); 
     }

     /**
      * @param \App\Models\Account $account
      * @param $tweetId
      * @return bool|null
      * @throws \Exception
      */
     protected function deleteFavorite(Account $account, $tweetId)
     {
         return Favorite::where([
             'tweet_id' => $tweetId,
             'account_id' => $account->id
         ])->delete();
     }
}

timelines.favorite_id をnullに更新し(①)、 favorites からデータを削除します(②)。
いいねを解除する機能はこれで実装完了です。

いいねを解除してみる

いいねを解除する際に使用するGraphQLのスキーマは次のとおりです。 リクエストにはいいね済みの timelines.idtweets.id を設定します。

mutation {
  UnMarkFavorite(tweet_id: 5, timeline_id: 6)
}

いいねの解除に成功すると下記のようなレスポンスが返却されます。

{
  "data": {
    "UnMarkFavorite": true
  }
}


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

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

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


Fin.


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

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

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


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