【Laravel】非同期処理(Ajax)を実装する|簡単な掲示板アプリの作成

Laravelでページを再読み込みしない「いいね機能」の実装方法を学びます。jQueryのAjaxを使い、ボタンクリックでサーバーと非同期通信を行います。コントローラーはJSON形式でデータを返し、JavaScriptがそのデータを使っていいね数やボタンの表示をリアルタイムに更新する、実践的な手順を初心者にも分かりやすく解説します。

作成日: 更新日:

開発環境

  • OS: Windows10
  • Visual Studio Code: 1.73.0
  • PHP: 8.3.11
  • Laravel: 11.29.0
  • laravel/breeze: 2.2

サンプルコード

/app/Http/Controllers/LikeController.php

/app/Http/Controllers/LikeController.php
1             $post->user->notify(new LikeNotification($post));
2         }
3 
4-        return redirect()->back();
5+        // レスポンスを返す
6+        return response()->json([
7+            'liked' => $post->likes()->where('user_id', $user->id)->exists(),
8+            'like_count' => $post->likes()->count(),
9+        ]);
10     }
11 }
12

/resources/views/layout.blade.php

/resources/views/layout.blade.php
1     <title>掲示板アプリ</title>
2     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
3     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
4+    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
5     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
6+    <meta name="csrf-token" content="{{ csrf_token() }}">
7 </head>
8 <body>
9     <nav class="navbar navbar-expand-lg bg-primary">
10

/resources/views/posts/show.blade.php

/resources/views/posts/show.blade.php
1     </div>
2 
3     {{-- いいねボタン --}}
4-    <form action="{{ route('posts.like', $post->id) }}" method="POST" style="display:inline;">
5-        @csrf
6-        <button type="submit" class="btn {{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}">
7-            {!! $post->likes->contains('user_id', auth()->id()) ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!}
8-        </button>
9-    </form>
10+    <button id="likeButton" class="btn {{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}">
11+        {!! $post->likes->contains('user_id', auth()->id()) ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!}
12+    </button>
13 
14     {{-- いいね数の表示 --}}
15-    <p>{{ $post->likes->count() }} 件のいいね</p>
16+    <p id="likeCount">{{ $post->likes->count() }} 件のいいね</p>
17 
18     {{-- コメント一覧 --}}
19     <h3>コメント一覧</h3>
20         {{ $comments->links() }}
21     </div>
22 
23+    {{-- JavaScriptで非同期通信の処理 --}}
24+    <script>
25+        $(document).ready(function() {
26+            $('#likeButton').on('click', function(e) {
27+                e.preventDefault();  // デフォルトのフォーム送信を防ぐ
28+
29+                const button = $(this);
30+                const likeCountElement = $('#likeCount');
31+
32+                $.ajax({
33+                    url: "{{ route('posts.like', $post->id) }}",
34+                    method: 'POST',
35+                    data: {},
36+                    headers: {
37+                        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
38+                    },
39+                    success: function(data) {
40+                        // サーバーから返された liked 状態に基づいてボタンのクラスを更新
41+                        if (data.liked) {
42+                            button.removeClass('btn-outline-danger').addClass('btn-danger');
43+                            button.html('<i class="bi bi-heart-fill"></i>');
44+                        } else {
45+                            button.removeClass('btn-danger').addClass('btn-outline-danger');
46+                            button.html('<i class="bi bi-heart"></i>');
47+                        }
48+
49+                        // いいね数を更新
50+                        likeCountElement.text(data.like_count + ' 件のいいね');
51+                    },
52+                    error: function(error) {
53+                        console.error('Error:', error);
54+                    }
55+                });
56+            });
57+        });
58+    </script>
59 @endsection
60

コード解説

変更点: コントローラーがJSON形式でデータを返すように変更

/app/Http/Controllers/LikeController.php
1-        return redirect()->back();
2+        // レスポンスを返す
3+        return response()->json([
4+            'liked' => $post->likes()->where('user_id', $user->id)->exists(),
5+            'like_count' => $post->likes()->count(),
6+        ]);

これまでの処理では、redirect()->back() を使ってページ全体を再読み込みしていました。非同期処理(Ajax)ではページを再読み込みせず、必要なデータだけをやり取りします。そこで、コントローラーの戻り値を変更します。

response()->json() は、PHPの配列をJSONというデータ形式に変換して返すLaravelの命令です。JSONはJavaScriptで非常に扱いやすい形式のため、非同期通信でよく利用されます。

ここでは、JavaScript側で画面の表示を更新するために必要な2つの情報を返しています。

  1. 'liked': 現在のユーザーがいいねをしているかどうかの状態(trueかfalse)。
  2. 'like_count': 最新のいいねの総数。

このJSONデータを受け取ったJavaScriptが、いいねボタンの見た目やいいね数を画面上で書き換える役割を担います。

変更点: jQueryの読み込み

/resources/views/layout.blade.php
1     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
2+    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
3     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>

非同期通信(Ajax)をより簡単に実装するために、JavaScriptのライブラリである「jQuery」を読み込みます。<script>タグを追加することで、WebページでjQueryの便利な機能が使えるようになります。この記述はアプリケーション全体のレイアウトファイルに追加しているため、どのページでもjQueryが利用可能になります。

変更点: CSRFトークンをmetaタグに設定

/resources/views/layout.blade.php
1     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
2+    <meta name="csrf-token" content="{{ csrf_token() }}">
3 </head>
4 <body>

Laravelは、セキュリティ対策としてCSRF(クロスサイトリクエストフォージェリ)という攻撃を防ぐ仕組みを持っています。通常のフォーム送信では@csrfと書くだけで自動的に対策用のトークンが埋め込まれますが、Ajax通信では手動で設定する必要があります。

ここでは、<meta>タグを使ってCSRFトークンをHTML内に埋め込んでいます。csrf_token()はLaravelが生成するユニークな文字列です。後ほど、JavaScriptがこの<meta>タグからトークンを読み取り、リクエストに含めてサーバーに送信することで、Laravelに「これは正当なリクエストです」と知らせることができます。

変更点: いいねボタンをフォームから独立したボタンに変更

/resources/views/posts/show.blade.php
1-    <form action="{{ route('posts.like', $post->id) }}" method="POST" style="display:inline;">
2-        @csrf
3-        <button type="submit" class="btn {{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}">
4-            {!! $post->likes->contains('user_id', auth()->id()) ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!}
5-        </button>
6-    </form>
7+    <button id="likeButton" class="btn {{ $post->likes->contains('user_id', auth()->id()) ? 'btn-danger' : 'btn-outline-danger' }}">
8+        {!! $post->likes->contains('user_id', auth()->id()) ? '<i class="bi bi-heart-fill"></i>' : '<i class="bi bi-heart"></i>' !!}
9+    </button>

これまではページを再読み込みする<form>タグでボタンを囲んでいましたが、AjaxではJavaScriptが通信を制御するため、<form>タグは不要になります。

代わりに、ただの<button>タグに変更し、id="likeButton"というID属性を追加しました。このIDは、JavaScriptが「どのボタンがクリックされたか」を正確に特定するための目印として使われます。

変更点: いいね数表示部分にIDを追加

/resources/views/posts/show.blade.php
1-    <p>{{ $post->likes->count() }} 件のいいね</p>
2+    <p id="likeCount">{{ $post->likes->count() }} 件のいいね</p>

いいねボタンがクリックされた後、JavaScriptはサーバーから受け取った最新のいいね数で画面の表示を更新する必要があります。

更新対象となるいいね数を表示している<p>タグをJavaScriptから特定できるよう、id="likeCount"というID属性を追加しました。これにより、JavaScriptは「この部分のテキストを書き換える」という指示を正確に行えるようになります。

変更点: Ajax通信を行うJavaScriptコードの追加

/resources/views/posts/show.blade.php
1+    {{-- JavaScriptで非同期通信の処理 --}}
2+    <script>
3+        $(document).ready(function() {
4+            $('#likeButton').on('click', function(e) {
5+                e.preventDefault();  // デフォルトのフォーム送信を防ぐ
6+
7+                const button = $(this);
8+                const likeCountElement = $('#likeCount');
9+
10+                $.ajax({
11+                    url: "{{ route('posts.like', $post->id) }}",
12+                    method: 'POST',
13+                    data: {},
14+                    headers: {
15+                        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
16+                    },
17+                    success: function(data) {
18+                        // サーバーから返された liked 状態に基づいてボタンのクラスを更新
19+                        if (data.liked) {
20+                            button.removeClass('btn-outline-danger').addClass('btn-danger');
21+                            button.html('<i class="bi bi-heart-fill"></i>');
22+                        } else {
23+                            button.removeClass('btn-danger').addClass('btn-outline-danger');
24+                            button.html('<i class="bi bi-heart"></i>');
25+                        }
26+
27+                        // いいね数を更新
28+                        likeCountElement.text(data.like_count + ' 件のいいね');
29+                    },
30+                    error: function(error) {
31+                        console.error('Error:', error);
32+                    }
33+                });
34+            });
35+        });
36+    </script>

ここが非同期処理を実現する中心部分です。追加されたJavaScriptコードは以下の順序で動作します。

  1. イベントの監視: $('#likeButton').on('click', ...)で、IDがlikeButtonの要素(いいねボタン)がクリックされるのを待ちます。
  2. Ajax通信の実行: ボタンがクリックされると、jQueryの$.ajax()関数が実行され、サーバーとの非同期通信が開始されます。
    • url: リクエストを送る先のURLをroute()で指定しています。
    • method: 'POST'でデータを送信することを指定します。
    • headers: リクエストのヘッダー情報です。ここで、先ほどHTMLに埋め込んだCSRFトークンを読み取り、X-CSRF-TOKENとして設定します。これにより、セキュリティエラーを防ぎます。
  3. 通信成功時の処理: success: function(data) { ... }の中の処理は、サーバーとの通信が成功し、レスポンスデータが返ってきたときに実行されます。
    • 引数dataには、コントローラーから返されたJSONデータ(likedの状態とlike_countの数)が入っています。
    • if (data.liked)の条件分岐で、likedtruefalseかに応じて、addClassremoveClassを使いボタンのCSSクラス(色)を切り替えます。また、html()を使ってボタン内のアイコンを変更します。
    • likeCountElement.text(...)で、data.like_countの値を使っていいね数の表示をリアルタイムで書き換えます。
  4. 通信失敗時の処理: error: function(error) { ... }は、通信に失敗した場合に実行され、開発者ツールのコンソールにエラーメッセージを表示します。

この一連の流れにより、ユーザーはページを再読み込みすることなく、スムーズに「いいね」とその結果の反映を体験できるようになります。

おわりに

お疲れ様でした。今回はjQueryのAjaxを使い、ページを再読み込みしない快適な「いいね機能」を実装しました。重要なポイントは、コントローラーがredirectでページ全体を返す代わりに、response()->json()でいいねの状態や総数といったデータだけを返すように変更した点です。そしてフロントエンドのJavaScriptがそのJSONデータを受け取り、idを目印にしてボタンの表示やいいね数をリアルタイムに更新しました。このようにサーバーとブラウザが役割を分担し、データ通信によって画面を動的に変更する流れが非同期処理の基本です。

関連コンテンツ