PHP/Laravel

Laravel6とAjaxで非同期いいね機能を作る方法【超丁寧に解説】

こんにちは、シロウ(@shiro_life0)です。

*10月31日に記事を修正致しました。

先日よりLaravelを使った掲示板サイトを作成していて、Ajaxを使ったいいね機能を実装しました。

そこで、同じようにリアルタイムないいね機能を実装しようと思っている方向けに実装方法を詳しく解説していきます。

完成形としては下記のような感じです。

たぶん、どこの記事よりもわかりやすいと思うのでぜひ参考にしてください(*´ω`*)ノ

【開発環境】

 

  • mac os
  • PHP 7.4.5
  • Laravel 6.18.40
  • Mysql
  • あとBoostrapとJavaScript/jQueryを使用します

開発環境は上記の通りです。
早速作っていきましょう!

今回作成するいいね機能の概要と事前準備

今回作成するいいね機能の概要は下記の通りです。

  • ユーザーがいいねすると画面を更新することなく色が変わる
  • 同時にいいねの総数も増えたり減ったりする
  • Ajaxを用いた非同期処理にする
  • Javascript/jQueryを「Laravel Mix」で利用する(vue.jsは使わない)

他の記事ではvue.jsを使ったものが多いですが、僕はまだvue.jsは勉強中なので今回はjqueryを利用することにしました。笑

Javascript/jqueryを「Laravel Mix」で利用する方法がわからない方は「Laravel Mix 使い方」でググればわかると思います!

簡単なので5分ほど調べて、理解してから読み進めるのがオススメですm(_ _)m

①テーブル設計を考える

今回は下記のようなテーブル設計にしました!

  • usersテーブルとpostsテーブルは1対多
  • usersテーブルとlikesテーブルは1対多
  • postsテーブルとlikesテーブルは1対多

さて、それでは実際にこれらのテーブルを設計していきましょう。

ただ、usersテーブルは既に作っていると思うので説明は割愛します。

postsテーブルを作成する

さて、それではまずはユーザーの投稿データを管理する「postsテーブル」を作成しましょう

まずはターミナルで下記コマンドを実行して、postsテーブルとPostモデルを作成してください。

php artisan make:model Post -m

オプションとして「-m」をつけると、マイグレーションファイルも同時に作成することができます。

おそらく「app > database > migrations」に「作成した日付_create_posts_table.php」ファイルができていると思います。

そのファイルを下記のように変更します。

<?php

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

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('title');
            $table->string('content');
            $table->string('category');
            $table->unsignedBigInteger('user_id');
            $table->timestamps();

            $table->foreign('user_id')
                    ->references('id')
                    ->on('users')
                    ->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('posts');
    }
}

「user_id」は「usersテーブルのid」と紐づかせるので外部キー制約も追加しています。

下記コマンドでマイグレーションを実行してみましょう。

php artisan migrate

無事にテーブルが完成していればOKです。

likesテーブルを作成する

次にlikesテーブルを作成します。

下記コマンドを実行してください。

php artisan make:model Like -m

次にマイグレーションファイルを下記のように変更します。

<?php

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

class CreateLikesTable extends Migration
{
    public function up()
    {
        Schema::create('likes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id');
            $table->unsignedBigInteger('post_id');

            $table->foreign('user_id')
                    ->references('id')
                    ->on('users')
                    ->onDelete('cascade');

            $table->foreign('post_id')
                    ->references('id')
                    ->on('posts')
                    ->onDelete('cascade');


            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('likes');
    }
}

コードを書き換えたらまた下記コマンドでマイグレーションを実行してください。

php artisan migrate

ひとまずこれでテーブル設計は完成です。

②:モデルに処理を記述

リレーションを記述していきます。

まずはUserモデル(User.php)に下記を追加してください。

    //User.phpに下記を追記
    // ユーザーの投稿
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    // ユーザーがいいねしている投稿
    public function likes()
    {
        return $this->hasMany(Like::class);
    }

 

続いて、Likeモデル(Like.php)に下記を記述。

     //Like.phpに下記を追記
     //いいねしているユーザー
     public function user()
    {
        return $this->belongsTo(User::class);
    }

     //いいねしている投稿
    public function post()
    {
        return $this->belongsTo(Post::class);
    }

    //いいねが既にされているかを確認
    public function like_exist($id, $post_id)
    {
    //Likesテーブルのレコードにユーザーidと投稿idが一致するものを取得
        $exist = Like::where('user_id', '=', $id)->where('post_id', '=', $post_id)->get();

        // レコード($exist)が存在するなら
        if (!$exist->isEmpty()) {
            return true;
        } else {
        // レコード($exist)が存在しないなら
            return false;
        }
    }

 

「like_exist()」は後ほど「いいねされているかどうか」の確認で使用するメソッドになります。

一応ここでメソッド内の記述について解説しますが、もう少し先で「like_exist()」を使用するところがあるので、そこまで読み進めてから読んだ方が理解しやすいかと思いますm(_ _)m

引数の「$id」には「ユーザーのid」、「$post_id」には「投稿のid」を渡して使用します。

Likesテーブルのレコードは「投稿がいいねされた時」に「いいねしたユーザーのid」と「いいねされた投稿のid」を1つのレコードに保存します。

そのため、引数に与えられた「ユーザーid」と「投稿id」の組み合わせのレコードが存在するなら「投稿には既にいいねがされている」、逆に一致するレコードがなければ「まだ投稿にいいねされていない」ということがわかります。

それを表した一文が「$exist = Like::where(‘user_id’, ‘=’, $id)->where(‘post_id’, ‘=’, $post_id)->get();」です。

また「isEmpty()」はコレクションの有無を調べることができる関数で、空ならtrue、空じゃない(存在する)ならfalseを返します。

そのため、「!$exist->isEmpty()」のように「!(エクスクラメーション)」をつけて、$existが空でない(存在する)ならtrueを返すようにしています。

 

続いて、Postモデル(Post.php)に下記を追記。

    //Post.phpに下記を追記
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function likes()
    {
        return $this->hasMany(Like::class);
    }

③:Ajaxの処理を書いていく

では早速Ajaxの処理をファイルに書いていきましょう。

まずは「app > resources > js > app.js」に下記を追記してください。

require('./ajaxlike.js')

次に「app > resources > js」フォルダの中に「_ajaxlike.js」ファイルを作成してください。*_(アンダーバー)が必要ですので忘れずに。

今作った「_ajaxlike.js」ファイルの中に下記を記述します。

$(function () {
var like = $('.js-like-toggle');
var likePostId;

like.on('click', function () {
    var $this = $(this);
    likePostId = $this.data('postid');
    $.ajax({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            },
            url: '/ajaxlike',  //routeの記述
            type: 'POST', //受け取り方法の記述(GETもある)
            data: {
                'post_id': likePostId //コントローラーに渡すパラメーター
            },
    })

        // Ajaxリクエストが成功した場合
        .done(function (data) {
//lovedクラスを追加
            $this.toggleClass('loved'); 

//.likesCountの次の要素のhtmlを「data.postLikesCount」の値に書き換える
            $this.next('.likesCount').html(data.postLikesCount); 

        })
        // Ajaxリクエストが失敗した場合
        .fail(function (data, xhr, err) {
//ここの処理はエラーが出た時にエラー内容をわかるようにしておく。
//とりあえず下記のように記述しておけばエラー内容が詳しくわかります。笑
            console.log('エラー');
            console.log(err);
            console.log(xhr);
        });
    
    return false;
});
});

コメントで解説していますが、もう少し補足します。

まずLaravelでajaxを利用するためにはCSRFトークンを設定する必要があるので、

headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            },

こちらが必要になります。
まぁ、これは絶対必要なおまじないと思っておけばOK。

次のurl: '/ajaxlike',は”の中にルーティグに合わせて記述します。
*ルーティングはあとで記述します。

data: {'post_id': likePostId},とすることでコントローラーの$requestに値が渡されるので、$request->post_idのようにすれば渡した値を使うことができます。

また、ajaxの通信が成功した時は.done(function (data) {})の中に処理を記述し、.fail(function () {})の中には通信が失敗した時の処理を書きます。

ちなみに引数にはコントローラーから返された値が入ります。

④:viewファイルを作成する

次にviewファイルを作成していきます。

ただviewファイルを全て記述すると面倒になるので、今回は「いいねマーク」のところのみのコードを紹介します。

 
@if($like_model->like_exist(Auth::user()->id,$post->id))
<p class="favorite-marke">
  <a class="js-like-toggle loved" href="" data-postid="{{ $post->id }}"><i class="fas fa-heart"></i></a>
  <span class="likesCount">{{$post->likes_count}}</span>
</p>
@else
<p class="favorite-marke">
  <a class="js-like-toggle" href="" data-postid="{{ $post->id }}"><i class="fas fa-heart"></i></a>
  <span class="likesCount">{{$post->likes_count}}</span>
</p>
@endif​

これをどこか適当なveiwファイルに貼り付けてください。

 

⑤:ルーティングを記述

次にルーティングを記述していきます。

<?php

Route::get('/', 'PostsController@index')->name('posts.index');

//ログイン中のユーザーのみアクセス可能
Route::group(['middleware' => ['auth']], function () {
    //「ajaxlike.jsファイルのurl:'ルーティング'」に書くものと合わせる。
    Route::post('ajaxlike', 'PostsController@ajaxlike')->name('posts.ajaxlike');
});

今回必要とするルーティングはこれだけです。

‘middleware’=>[‘auth’]とすることで、ログイン中のユーザーのみアクセスできるようになります。

⑥:コントローラーに記述

次にPostsContoroller.phpに処理を記述していきます。

最終的には下記のようになります。

<?php

namespace App\Http\Controllers;

use App\Post;
use App\Like;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;

class PostsController extends Controller
{
    public function index()
    {
        $data = [];
        // ユーザの投稿の一覧を作成日時の降順で取得
        //withCount('テーブル名')とすることで、リレーションの数も取得できます。
        $posts = Post::withCount('likes')->orderBy('created_at', 'desc')->paginate(10);
        $like_model = new Like;

        $data = [
                'posts' => $posts,
                'like_model'=>$like_model,
            ];

        return view('posts.index', $data);
    }

        public function ajaxlike(Request $request)
    {
        $id = Auth::user()->id;
        $post_id = $request->post_id;
        $like = new Like;
        $post = Post::findOrFail($post_id);
        //loadCountとすればリレーションの数を○○_countという形で取得できる(今回の場合はいいねの総数)
        $postLikesCount = $post->loadCount('likes')->likes_count;

        // 空でない(既にいいねしている)なら
        if ($like->like_exist($id, $post_id)) {
            //likesテーブルのレコードを削除
            $like = Like::where('post_id', $post_id)->where('user_id', $id)->delete();
        } else {
            //空(まだ「いいね」していない)ならlikesテーブルに新しいレコードを作成する
            $like = new Like;
            $like->post_id = $request->post_id;
            $like->user_id = Auth::user()->id;
            $like->save();
        }

        //一つの変数にajaxに渡す値をまとめる
        //今回ぐらい少ない時は別にまとめなくてもいいけど一応。笑
        $json = [
            'postLikesCount' => $postLikesCount,
        ];
        //下記の記述でajaxに引数の値を返す
        return response()->json($json);
    }
}

少し解説しておくと、index()はただ単にユーザーの情報とLikeインスタンスを作成して、indexファイルに返しているだけです。

ポイントはwithCount()ですが、これを使えば○○__countとすることでリレーションの数を読み込めます。(今回の場合は投稿に紐づいているLikesテーブルのレコードの数が取得出来ます。)

viewファイルの{{$post->likes_count}}としているところがこれに該当します。

次にajaxlike()の解説についてですが、ほぼコメント通りです。

ただ、こちらではリレーションの数を取得するのにloadCount()を使っています。これは特に意味はなく僕のミスです。笑

withCount()かloadCount()どちらか1つに統一した方がわかりやすかったですが、今回はこのまま進めます。

return response()->json()とすることで「ajaxlike.jsファイル」にパラメーターを返すことができます。

今回の場合は$postLikesCountつまりいいねの総数を返しています。

このようにすることで、通信が成功した時に先ほど記述した下記コードが実行されます。

.done(function (data) {
    //lovedクラスを追加
    $this.toggleClass('loved'); 

    //.likesCountの次の要素のhtmlを「data.postLikesCount」の値に書き換える
    $this.next('.likesCount').html(data.postLikesCount); 

})

この引数(今回はdata)に先ほどreturn response()->json()で返した値(今回はpostLikesCount)が入っています。

なので、$this.next('.likesCount').html(data.postLikesCount); とすることで、likesCountクラスの要素の次の要素のhtmlをいいねの総数(postLikesCount)に書き換えています。

この処理がなかったら、いいねをしたり、取り消したりしているのに色だけ変わって、いいねの総数が増減しないという不思議なことになってします。笑

また、toggleクラスの要素にloved要素を追加することで色をリアルタイムで変更します。

そのためcssでlovedクラスの色を変更するための下記コードを描きましょう。

.loved i {
  color: red !important;
}

これで完璧です。

 

少し長くなってしまいましたが、これにてリアルタイムないいね機能が実装できたかと思います。

もしうまくいかなかったという方は僕のツイッターにご連絡くださいませ。

Twitter  =>  shiro_life0