Lesson13 Laravel応用

01Laravelでメモ共有アプリを作成する

このレッスンでは、先ほど作成したメモ帳をさらに拡張していきます。

先ほどのメモ帳では、全員が同じメモ帳を使うことになってしまい、誰もが使えるような作りになっていません。

ここでは、認証機能を実装し、ユーザーごとにメモを表示切替できるようなアプリを作成していきます。また、フォロー機能やお気に入り機能を実装し、X(旧:Tweitter)のようなアプリに仕上げていきます。

02アプリの完成イメージ

トップページ

タイムライン

ユーザープロフィール画面

03Laravelのインストール(cloud9)

Laravelのインストールを行います。

アプリケーションの名前は「share_memo」にしていただき、あとは「Lesson9 Laravel」教材にある「Laravelのインストール(cloud9)」を参考にアプリケーションの作成を行って下さい。

04ログイン(認証)機能の実装

早速ですがログイン機能を実装していきます。

PHPだとログイン機能を実装すると、ログイン画面やログイン処理、ユーザー登録処理、パスワードリセット処理など様々なことを1から記述しなければなりません。

しかし、Laravelではコマンド1つで、ログイン機能を実装することができます。

ターミナルを開き以下のコマンドを上から順に実行していってください。

(必ずshare_memoディレクトリにいることを確認してから実行してください)

// share_memoに移動
cd share_memo

composer require laravel/ui

php artisan ui vue --auth

npm install

npm run build

php artisan migrate

上記が実行できたら画面を再読み込みしましょう。

右上に「Log in」「Register」ボタンが追加されました。

これでログイン機能が自動で実装されました。

今のままではテーブルが存在しないのでテーブルを作成します。

以下のコマンドを実行し、テーブルを作成しましょう。

php artisan migrate

ターミナルが以下のようになれば成功です。

では「Register」ボタンをクリックし、ユーザーを登録してみましょう。

ユーザーが登録されたらホームが表示されます。

下記の画像のようなページが表示されたらユーザー作成と登録ができています。

テーブルも確認してみましょう。

mysqlにアクセスし、「users」テーブルを確認しましょう。

「users」テーブルを確認すると先ほど作成したユーザーが登録されています。

ログイン機能については以上で実装完了です。

詳しいログイン機能については以下の記事を参考にしてください。

本気で詳細を理解したい人向けのLaravelログイン認証 | アールエフェクト

05ヘッダーの作成

以下のコードを追記して、ログアウトボタンを設置します。

home.blade.php

<body>
  {{-- ここから追加 --}}
  <header>
    <div class="container">
      <div class="d-flex header_flex">
        <div class="left_header"><h2>メモ共有</h2></div>
        <div class="right_header">
          <ul>
            <li>
              <form action="{{ route('logout') }}" method="post">
                @csrf
                <input class="logout_btn" type="submit" value="ログアウト" />
              </form>
            </li>
            <li>ユーザー名:{{ Auth::user()->name }}</li>
          </ul>
        </div>
      </div>
    </div>
  </header>
  {{-- ここまでを追加 --}}
  <div class="container">
    <div class="memo_area"><div class="memo_form"></div></div>
  </div>
</body>

メモ帳作成時に使った、public/css/style.cssをコピペしてください。

その後以下のコードを最終行に追加します。

header {
    height: 80px;
}
.header_flex {
    display: flex;
    justify-content: space-between;
}
.left_header {
    width: 30%;
}
.right_header {
    width: 70%;
    display: flex;
    justify-content: end;
}
.right_header ul {
    list-style: none;
    display: flex;
}
.right_header ul li {
    margin-left: 20px;
}
.right_header ul li a {
    text-decoration: none;
    color: black;
    font-weight: bold;
}
.logout_btn {
    background-color: transparent;
    border: none;
    font-weight: bold;
}

06メモ帳をコピペする

先ほどのレッスンで作成したメモ帳を今回作成した「share_memo」にコピペしていきましょう。

コピペ対象は以下のファイルです。

  • web.php // 一部のみコピペ
  • home.blade.php
  • edit.blade.php
  • MemoController.php
  • Models/Memo.php

web.phpは以下のようにコピペしてください。

Auth::routes(); Route::group(['middleware' => 'auth'], function () {
    Route::get('/', 'App\\Http\\Controllers\\MemoController@show');
    Route::post('/add', 'App\\Http\\Controllers\\MemoController@add');
    Route::post('/delete', 'App\\Http\\Controllers\\MemoController@delete');
    Route::get('edit/{edit_id}', 'App\\Http\\Controllers\\MemoController@getEdit');
    Route::post('update', 'App\\Http\\Controllers\\MemoController@postEdit');
});

RouteServiceProvider.php(ログイン後のURL変更)

public const HOME = '/'; // '/home'から変更

ルートファイルのRoute::group(['middleware' => 'auth'], function () {});の部分を解説します。

07ミドルウェアとは?

LaravelのMiddlewareは、HTTPリクエストを処理する際にリクエストとレスポンスの中間に割り込む仕組みです。 Middlewareを使用することでリクエストに対して、フィルタリング、リクエストされる値の変換、ログ記録などの共通処理を行うことができます。

08ミドルウェアの使用用途

認証

ユーザーの認証状態を確認するために使用します。 ログインしていないユーザーに対して特定のページやアクションのアクセス制限を設けることが可能です。

ログ記録

リクエストやレスポンスのログを記録するためにMiddlewareを使用することでアプリケーションの動作を監視し、不具合が発生した場合に解決に役立てることができます。

リクエストの変換

リクエストのデータを変換したりバリデーションを行ったりする処理が可能です。

リクエストの特定のフィールドを整形したり、特定のルールに従ってバリデーションエラーを発生させたりすることが可能です。

レスポンスの変換

レスポンスを加工して特定のフォーマットに整形することが可能です。

JSONやXMLなどのフォーマットにレスポンスを変換するといったことができます。

ミドルウェア実装方法

※以下のミドルウェアに関するコマンド・ソースコードは記載例であり、どこかに記載する必要はありません。

artisanコマンドでミドルウェアクラスを作成

php artisan make:middleware AuthenticateMiddleware

AuthenticateMiddlewareクラスが作成されるのでhandle()に実際の処理を記述します。 各使用例に応じた具体的な実装方法は後述します。

class AuthenticateMiddleware { public function handle($request, Closure $next) {
    // リクエストを処理する前のMiddlewareの共通処理 //
    例:ログ記録、認証チェックなど $response = $next($request); //
    レスポンスを加工する処理(必要な場合のみ) return $response; }
}

09ミドルウェアを使用できるように設定する

特定のルートにMiddlewareを適用する

ルートに割り当てるためにapp/Http/Kernel.phpの$routeMiddleware配列に設定します。

protected $routeMiddleware = [
    'authenticateMiddleware' =>
    \\App\\Http\\Middleware\\AuthenticateMiddleware::class,
];

middlewareメソッドを使用してミドルウェアをルートに割り当てることができます。

Route::get('/index', function () {
    //
})->middleware('authenticateMiddleware');

特定のルートに複数Middlewareを適用する

app/Http/Kernel.phpの$middlewareGroups配列に登録します。

protected $middlewareGroups = [
    'web' => [
        \\App\\Http\\Middleware\\EncryptCookies::class,
        \\App\\Http\\Middleware\\VerifyCsrfToken::class
    ],
]

ミドルウェアがHTTPカーネルで定義したらmiddlewareメソッドを使用してミドルウェアをルートに割り当てることができます。

web.php

Route::get('/index', function () {
    //
})->middleware('web');

複数のMiddlewareを指定することもできます。

Route::post('/index', function () {
    //
})->middleware(['web', 'authenticateMiddleware']);

複数ルートにMiddlewareを適用する

groupメソッドを使用することで複数のルートにMiddlewareをまとめて適用させることができます。

Route::group(['middleware' => ['authenticateMiddleware']], function () {
    Route::get('/index', function () {
    })
    Route::post('/index', function () {
    })
}

10ミドルウェアの優先順位を設定する

protected $middlewarePriority = [
    \\App\\Http\\Middleware\\AuthenticateMiddleware::class,
    \\App\\Http\\Middleware\\LogRequestMiddleware::class,
];

このように設定することでAuthenticateMiddlewareLogRequestMiddlewareより先に実行されるようになります。

11ミドルウェアの使用例

12認証チェック

例:ユーザーが認証済みでなかった場合にログインページにリダイレクトさせる処理

class AuthenticateMiddleware {
    public function handle($request, Closure $next) {
        // 認証済みかチェック
        if (!Auth::check()) {
            return redirect()->route('login'); // ログインページにリダイレクト
        } return $next($request);
    }
}

13ログ記録

例:不具合発生時のトラブルシューティングのためにアクセスされたURLをログに出力させる処理

class LogRequestMiddleware {
    public function handle($request, Closure $next) {
        // リクエストされたURLをログに出力
        Log::info($request->url());
        return $next($request);
    }
}

14リクエストの変換

例:ユーザーからリクエストされたデータを加工する処理

class ConvertRequestMiddleware {
    public function handle($request, Closure $next) {
        // リクエストパラメータに値を追加する
        $request->merge(['hoge' => 'テスト']);
        return $next($request);
    }
}

15レスポンスの変換

例:ユーザーにレスポンスを返す際に形式を変更したうえでレスポンスを返す処理

class ConvertResponseToJsonMiddleware {
   public function handle($request, Closure $next) {
     $response = $next($request);
     // レスポンスをJSON形式に変換
     $data = $response->original;
     $jsonResponse = response()->json($data);
     return $jsonResponse;
   }
}

続いてメモテーブルを作成していきます。

以下のコマンドを実行してマイグレーションファイルを作成します。

php artisan make:migration create_memo_table

作成したファイルのupメソッドを以下のように修正してください。

public function up(): void {
    Schema::create('memo', function (Blueprint $table) {
        $table->id();
        $table->integer('user_id')->nullable(false);
        $table->string('content', '255')->nullable(false);
        $table->integer('invalid')->default(0);
        $table->timestamps();
    });
}

先ほど作成したメモ帳の時のマイグレーションファイルとは内容が異なるので注意してください。

前回から増えたカラムは「user_id」「invalid」です。

まずはuser_idについてです。

user_idは「users」テーブルのidが入ります。

このような関係を1対多と言います。詳しくは以下で解説しています。

161対多とは

X(旧:Twitter)のようなSNSサービスで考えてみましょう。

あるユーザーが1日に5回ツイートした場合、このユーザーは3つのツイートを持っていることになります。このようにあるユーザー1人に対して複数ツイートを持っているような関係を1対多の関係と呼びます。

図で表すと以下の通りです。

こちらをER図にすると以下の通りです。

17Laravelで実装

リレーションを定義

自テーブルから見て複数対応関係にある場合(親→子の関係)はhasMany()を使用します。(User → Tweet)

hasMany()の使い方

hasMany('関係するモデル', '関係するモデルで対応しているカラム名', '自テーブルで対応しているカラム名');

/* 省略 */
class User extends Model {
    public function tweets() {
        // 第一引数:紐づける先のモデル
        // 第二引数:相手テーブルのカラム名
        // 第三引数:自テーブルのカラム名
        return $this->hasMany(Tweet::class, 'user_id', 'id');
    }
}

逆のパターン(子→親の関係)はbelongsTo()を使用します。(Tweet → User)

belongsTo()の使い方

belongsTo('関係するモデル', '関係するモデルで対応しているカラム名', '自テーブルで対応しているカラム名');

/* 省略 */
class Tweet extends Model {
    public function user() {
        // 第一引数:紐づける先のモデル
        // 第二引数:相手テーブルのカラム名
        // 第三引数:自テーブルのカラム名
        return $this->belongsTo('User', 'user_id', 'id');
    }
}

Userに紐づくTweetを取得する方法

$tweets = User::find(1)->tweets->toArray();

/*
    $tweetsの中身例 
    [
        [
            'id' => 1,
            'user_id' => 1,
            'tweet' => 'hogehoge'
        ],
        [
            'id' => 2,
            'user_id' => 1,
            'tweet' => 'fugafuga'
        ]
    ]
*/

Tweetが誰のものか取得する方法

$user = Tweet::find(1)->user;
/*
    $userの中身例
    ※厳密にいうとuserは配列ではないが、見やすくするために配列っぽく書いてます。
    [
        'id' => 1,
        'name' => '太郎'
    ]
*/

18多対多とは

再びTwitterのようなSNSサービスで考えてみましょう。

あるユーザーが「#松」「#竹」「#梅」という3つのハッシュタグを付けたツイートをする場合、このツイートは3つのハッシュタグを持っていることになります。 さらに、「#松」というハッシュタグが付けられているツイートも複数ツイート存在しています。

このような関係を多対多と呼びます。

図で表すと以下の通りです。(TweetAがハッシュタグを3つ持っている)

松から見た図で表すと以下の通りです。(同じハッシュタグが付けられているTweetが3つある)

こちらをER図にすると以下の通りとなります。

↑このテーブル定義で実装を進めていくと、tags.tweet_idは一つのtweet.idしか保持することができないため、多対多を表現することは難しくなってしまいます。(カラムを追加するなどすれば対応できるがよろしくない。)

このような問題を防ぐために中間テーブルというものを用いて多対多を表現していくことになります。

中間テーブル

中間テーブルは接続先のテーブルそれぞれの主キーを外部キーとして持たせる

今回の例でいうと、tagsテーブルとtweetsテーブルの主キーを中間テーブルに外部キーtweet_id、tag_idとして持たせます。

ER図にすると以下の通りになります。

19Laravelで実装

リレーションを定義

多対多の場合はbelongsToManyを使用します。

使い方

belongsToMany('関係するモデル', '中間テーブルのテーブル名', '中間テーブル内で対応しているカラム名', '関係するモデルで対応しているカラム名');

実際にモデルに定義します。

/*
    省略
*/
class Tag extends Model
{
    public function tweets() {
        // 第一引数:紐づける先のモデル(※中間テーブルではない)
        // 第二引数:中間テーブル名
        // 第三引数:中間テーブル内で対応しているカラム名(自テーブルのidと中間テーブルのtag_idで紐づく)
        // 第四引数:関係するモデルで対応しているカラム名(結合先テーブルのidと中間テーブルのtweet_idで紐づく)
        return $this->belongsToMany(Tweet::class, 'tag_tweets', 'tag_id', 'tweet_id');
    }
}

/*
    省略
*/
class Tweet extends Model
{
    public function tags() {
        // 第一引数:紐づける先のモデル(※中間テーブルではない)
        // 第二引数:中間テーブル名
        // 第三引数:中間テーブル内で対応しているカラム名(自テーブルのidと中間テーブルのtweet_idで紐づく)
        // 第四引数:関係するモデルで対応しているカラム名(結合先テーブルのidと中間テーブルのtag_idで紐づく)
        return $this->belongsToMany(Tag::class, 'tag_tweets', 'tweet_id', 'tag_id');
    }
}

Tagに紐づくTweetを取得する方法

$ tweets = Tag :: find(1) -> tweets -> toArray();
/*
    $tweetsの中身例
    [
        [
            'id' => 1,
            'tweet' => 'hogehoge'
        ],
        [
            'id' => 2,
            'tweet' => 'fugafuga'
        ]
    ]
*/

Tweetに紐づくTagを取得する方法

$tags = Tweet::find(1)->tags->toArray();
/*
$tagsの中身例
[
  [
    'id' => 1,
    'name' => '#foo'
  ],
  [
    'id' => 2,
    'tweet' => '#foobar'
  ]
]
*/

つづいて「invalid」ですが、SQLの時に解説した論理削除を用いるためのカラムです。

削除には物理削除と論理削除があり、物理削除はレコード自体をDELETE文などで消してしまう削除方法です。メモアプリの時も物理削除で実装しました。

今回は論理削除で実装していきます。

論理削除はレコード自体を削除するのではなく、削除しているかどうかのフラグとなるカラムを用意し、そのカラムで削除されているかを判別する削除方法です。

例えば今回では「invalid」が削除判別フラグの役割を担います。invalidに0が入っているレコードは削除されていないレコード、1が入っているレコードは削除されたレコードして扱います。

当然テーブルから取得するときに、Memo::get();としてしまうと削除されたとされているレコードも取得されますが、Memo::where(’invalid’, 0)→get();とすることで、invalidが0のレコードだけ取得することができます。

今回はすべてのテーブルにinvalidを含めます。

それではマイグレーションを実行しましょう。

php artisan migrate

データベースを確認してみましょう。

// ターミナルが mysql> になってない場合
sudo mysql -u root

//
DESCRIBE memo;

memoテーブルの構成が表示されれば無事に作成されています。

画面を読み込んでみましょう。

メモは表示されていませんが先ほど作成したメモ帳が表示されました。

今のままではメモを追加できないので(テーブルのカラムを増やしたため)追加処理の部分を修正していきましょう。

MemoController.phpのadd()メソッドを修正

use Illuminate\\Support\\Facades\\Auth; // 追加

// 省略

public function add(Request $request) {
    // ユーザーIDを取得
    $user_id = Auth::id();

    // 追加
    $memo_text = $request->memo_text;
    $memo_model = new Memo();
    $memo_model->content = $memo_text;
    $memo_model->user_id = $user_id; // 追加

    $memo_model->save(); return redirect('/'); // 変更
}

これでメモが追加されるようになりました。

画面を読み込んでメモを追加してみましょう。

無事に追加できました。

コードを解説します。

$user_id = Auth::id();まずはこの部分ですが、Authファサードを使うと、ログインしているユーザーの情報を取得することができます。

今回はidだけ取得したいので、Auth::id()としています。

ユーザー名やメールアドレスなど他の情報を取得したい場合はAuth::user();で情報を一括で取得できます。

どのユーザーがメモを追加したかを判別するためユーザーIDを取得しています。

return redirect('/');この部分は、前回までself::show();でした。この状態だとURLが/addになり、再読み込みすると、読み込んだ回数分メモが増殖してしまいます。

増殖を防止するためにURLを/にリダイレクトさせます。

/にリダイレクトされるとshow()メソッドが実行されるので、挙動は同じになります。

20別ユーザーの追加

それでは、ログアウトボタンをクリックしログアウトした後ユーザーをもう1つ作成しましょう。

ログイン後の画面を見てみましょう。

さきほど別のアカウントで作成したメモが表示されています。

他のアカウントの情報が表示されるのはセキュリティ上よくないことです。

自分のアカウントのメモだけが表示されるように修正していきましょう。

MemoController.phpのshow()メソッドに追記します。

public function show() {
    $user_id = Auth::id(); // 追記
    $memo_info = Memo::where('user_id', $user_id)->where('invalid', 0)->get(); // 追記
    
    return view('home')
        ->with('memo_info', $memo_info);
}

メモアプリではMemo::get();とすべてのメモ内容を取得していましたが、今回は、WHERE句でuser_idを設定することで、ログインしているアカウントが作成したメモのみを取得しています。

さらに、論理削除されていない、invalidカラムが0のメモに絞ることで、削除されていないログインしているユーザーのメモのみを取得することができます。

画面を再読み込みしましょう。

画面に表示されなくなりました。

再度ログアウトして、最初のアカウントにログインしてください。

無事にアカウント別で表示内容を切り替えることができました。

では続いて削除機能と編集機能を修正していきます。

21論理削除の実装

現状ではメモが物理削除されてしまいます。

今回は、invalidというカラムを追加し0なら未削除、1なら削除ときり分ける論理削除で実装しています。

物理削除を論理削除に修正していきましょう。

public function delete(Request $request) {
    $user_id = Auth::id(); // 追記
    $delete_id = $request->delete_id;
    Memo::where('user_id', $user_id)
        ->where('id', $delete_id)->update(['invalid' => 1]); // 変更

    return redirect('/') // 変更
}

コードの解説をします。

Memo::where('user_id', $user_id)->where('id', $delete_id)->update(['invalid' => 1]);

この部分では、user_idとidでレコードを絞った後、update文でinvalidを0から1に変更しています。

DBからは削除されませんが画面上では表示されません。

削除ボタンを押してメモを削除してみましょう。

メモが削除されました!

// ターミナルが mysql> になってない場合
sudo mysql -u root

SELECT * FROM 'memo';

テーブルではメモが残っています。

invalidのカラムに注目してください。1が入っており、削除されている状態になっています。

画面に表示されないのはshow()メソッドでwhere(’invalid’, 0)と絞り込みをしているからです。

22編集機能の修正

編集機能部分は大きく修正しません。

以下のように修正してください。

MemoController.php の postEditメソッド

public function postEdit(Request $request) {
    $user_id = Auth::id();
    $edit_id = $request->edit_id;
    $edit_memo = $request->edit_memo;

    Memo::where('id', $edit_id)
        ->where('user_id', $user_id)->update(['content' => $edit_memo]);

    return redirect('/');
}

実際に編集してみましょう。

23タイムライン機能の実装

続いて、他のユーザーのメモも見れるようなタイムライン機能を実装していきましょう。

要件は以下です。

  • 他のユーザーのメモを見れるようにする
  • 自分のメモのみ編集、削除できるようにする
  • メモにユーザー名を表示させる
  • ヘッダーメニューに「タイムライン」を追加する

イメージ画面

ヘッダーメニューの部分ですが、「トップ」に先ほどまで作成していたメモ追加画面を移動しています。

「トップ」ページ

まずはタイムラインを作成する前にヘッダーメニューに「トップ」を追加し、メモ追加画面が見られるようにしましょう。

メニューを追加したいのですが、ヘッダーメニューの部分はどのページにでも共通です。

例えば、タイムラインの新規ページを作ったとして、タイムラインページとトップページのヘッダーメニューは同じになりますよね?

同じヘッダーメニューなのにトップ・タイムラインページそれぞれでHTMLをべた書きしていた場合、新しくメニューが追加されたり、メニュー名が変更されたときにトップ・タイムラインページの2つのブレードファイルを編集しなければいけなくなります。

これでは面倒ですし保守性の面でもよくありません。

ですので、ブレードファイルで共通になる部分は共通化することができます。

それがLaravelに標準搭載されている「テンプレートエンジンであるblade」というものです。

ブレードファイルでは様々な便利メソッドが用意されています。

早速使ってみましょう。

resources/views/layoutsフォルダの中に(なければ作成してください)base.blade.phpというブレードファイルを作成しましょう。

作成できたら以下をコピー&ペーストしてください。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>メモ帳</title>
    <link rel="stylesheet" href="{{ asset('css/style.css') }}">
  </head>
  <body>
    <header>
      <div class="container">
        <div class="d-flex header_flex">
          <div class="left_header">
            <h2>メモ共有</h2>
          </div>
          <div class="right_header">
            <ul>
              <li>
                <a href="/">トップ</a>
              </li>
              <li>
                <a href="">タイムライン</a>
              </li>
              <li>
                <form action="{{ route('logout') }}" method="post">@csrf 
                  <input class="logout_btn" type="submit" value="ログアウト">
                </form>
              </li>
              <li>
                <a href="">ユーザー名:{{ Auth::user()->name }}</a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </header>
    <div class="container">@yield('content') </div>
  </body>
</html>

続いて home.blade.phpを以下のように編集します。

@extends('layouts.base') @section('content') 
<div class="memo_area">
  <div class="memo_form">
    <h2>メモを追加</h2>
    <form action="{{ asset('/add') }}" method="post">@csrf 
      <input class="memo_text" type="text" name="memo_text" id="memo_text">
      <input type="submit" value="追加">
    </form>
    <div class="search_area" style="margin-top: 50px">
      <h2>検索</h2>
      <form action="">@csrf 
        <input class="memo_text" type="text" name="search_word" id="search_word">
        <input type="submit" value="検索">
      </form>
    </div>
  </div>
  <div class="memo_show">@foreach($memo_info as $memo) 
    <div class="memo_item">
      <div class="memo_title">
        <time>{{$memo->created_at}}</time>
        <p>{{$memo->content}}</p>
      </div>
      <div class="btn_area">
        <div class="edit_form">
          <form action="{{ asset('/edit/'.$memo->id) }}" method="get">@csrf 
            <input type="submit" value="編集">
          </form>
        </div>
        <div class="del_area">
          <form action="{{ asset('/delete') }}" method="post">@csrf 
            <input type="hidden" name="delete_id" value="{{$memo->id}}">
            <input type="submit" value="削除">
          </form>
        </div>
      </div>
    </div>@endforeach 
  </div>
</div>@endsection

コードを解説していきます。

base.blade.phpが今回共通化する部分です。

今回共通化しているのは、ヘッドのメタ情報の部分とヘッダーメニューの部分です。

home.blade.phpを見てみましょう。<head>タグの部分や<body>タグの部分がなくなっています。

それらの記述はすべてbase.blade.phpに移動させています。

base.blade.phpの下の方に@yield('content')があります。

この部分がhome.blade.phpの@section(’content’) ~ @endsectionの部分を呼び出しています。

ですので、2つのブレードファイルを使用していますが画面に出力される際には以下の1つのファイルとして出力されます。

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>メモ帳</title>
    <link rel="stylesheet" href="{{ asset('css/style.css') }}">
  </head>
  <body>
    <header>
      <div class="container">
        <div class="d-flex header_flex">
          <div class="left_header">
            <h2>メモ共有</h2>
          </div>
          <div class="right_header">
            <ul>
              <li>
                <a href="/">トップ</a>
              </li>
              <li>
                <a href="">タイムライン</a>
              </li>
              <li>
                <form action="{{ route('logout') }}" method="post">@csrf 
                  <input class="logout_btn" type="submit" value="ログアウト">
                </form>
              </li>
              <li>
                <a href="/user_profile/{{Auth::id()}}">ユーザー名:{{ Auth::user()->name }}</a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </header>
    <div class="container">
      <div class="memo_area">
        <div class="memo_form">
          <h2>メモを追加</h2>
          <form action="{{ asset('/add') }}" method="post">@csrf 
            <input class="memo_text" type="text" name="memo_text" id="memo_text">
            <input type="submit" value="追加">
          </form>
          <div class="search_area" style="margin-top: 50px">
            <h2>検索</h2>
            <form action="">@csrf 
              <input class="memo_text" type="text" name="search_word" id="search_word">
              <input type="submit" value="検索">
            </form>
          </div>
        </div>
        <div class="memo_show">@foreach($memo_info as $memo) 
          <div class="memo_item">
            <div class="memo_title">
              <time>{{$memo->created_at}}</time>
              <p>{{$memo->content}}</p>
            </div>
            <div class="btn_area">
              <div class="edit_form">
                <form action="{{ asset('/edit/'.$memo->id) }}" method="get">@csrf 
                  <input type="submit" value="編集">
                </form>
              </div>
              <div class="del_area">
                <form action="{{ asset('/delete') }}" method="post">@csrf 
                  <input type="hidden" name="delete_id" value="{{$memo->id}}">
                  <input type="submit" value="削除">
                </form>
              </div>
            </div>
          </div>@endforeach 
        </div>
      </div>
    </div>
  </body>
</html>

また、注意ですが読み込まれる側のファイル(今回だとhome.blade.php)の1行目には必ず@extends('layouts.base')を記述するようにしましょう。

これはlayoutsフォルダの中のbase.blade.phpを読み込みますよという意味になります。

@yieldの使い方ですが、@yield(’名前’)とすると、@section(’名前’) ~ @endsectionの部分が自動的に@yieldの部分に読み込まれます。

※名前の部分は必ず同じである必要があります。

逆に言うと名前を変えると何個であっても@yieldは使うことができます。

よく実務でも使われる例を紹介します。

layouts/base.blade.php

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">@yield('link') 
    <title>Document</title>
  </head>
  <body>@yield('content') </body>
</html>

home.balde.php

@extends('layouts.base') @section('link') 
<link rel="stylesheet" href="{{ asset('css/home.css') }}" >
<script src="{{ asset('js/index.js') }}"></script>@endsection @section('content') 
<div>// ページの中身 
<div>@endsection

home.blade.php を見てみると@sectionが2つありますね。

@section(’content’)は先ほど説明した通り@yields(’content’)の部分に読み込まれます。

注目してほしいのは@yields(’link’)の部分です。

<head>の中に記述されています。これは、ページによって独自のCSS・JSなどのファイルを読み込ませたい時が多くあります。

そこで@yields(’link’)と@section(’link’) ~ @endsectionを組み合わせることで簡単にファイルごとに読み込ませたいものを変えることができます。

この使い方は様々な部署で使われているので覚えておきましょう。

この状態でページを読み込んでみましょう。

ちゃんとページが表示されましたね!

ではいよいよタイムラインページを作成していきます。

新規ページを作成する流れは以下です。

  1. ブレードファイルを作成
  2. URLを決める
  3. コントローラを作成し、ページを表示させるだけのメソッドを作成
  4. web.phpにルーティングを追加し③で作成したメソッドが実行されるようにする
  5. 2で設定したURLに遷移する

ではまず1.ブレードファイルを作成してみましょう。

resources/viewsのフォルダの中に新しく timeline.blade.php を作成しましょう。

@extends('layouts.base') @section('content') 
<div class="memo_area">
  <div class="memo_show">タイムライン </div>
</div>@endsection

タイムラインという文字列のみ表示されるシンプルなページです。

次に2.のURLを設定を行います。

ヘッダーメニューの部分に「タイムライン」メニューを追加しました。その部分にURLを設定しましょう。

base.blade.php

<li>
    <a href="/timeline">タイムライン</a> // hrefに追記
</li>

次に3.コントローラを作成し、ページを表示させるだけのメソッドを作成します。

ターミナルで以下のコマンドを実行しましょう。

php artisan make:controller TimeLineController

TimeLineController.phpが作成されたと思います。

TimeLineController.phpの中に以下のメソッドを追記してください。

public function show() {
    return view('timeline');
}

ブレードファイルを表示させるためだけのシンプルなメソッドです。

では続いて4.web.phpにルーティングを追加し③で作成したメソッドが実行されるようにしましょう。

web.php

Route::get('/timeline', 'App\\Http\\Controllers\\TimeLineController@show');

では最後に⑤②で設定したURLに遷移してみましょう。

ヘッダーメニューのタイムラインをクリックしてください。

上記の様に表示されればOKです。

では機能を足していきましょう。

その前に要件を再度確認します。

  • 他のユーザーのメモを見れるようにする
  • 自分のメモのみ編集、削除できるようにする
  • メモにユーザー名を表示させる
  • ヘッダーメニューに「タイムライン」を追加する

です。

一番の機能としては「他のユーザーのメモを見れるようにする」です。

メモ追加機能では自分のメモのみを取得しましたが今回は他のユーザーも表示させる必要があります。さらに「メモにユーザー名を表示させる」という要件ですが、ユーザー名は「users」テーブルにあり、メモの内容は「memo」テーブルにあります。

つまりページに表示されたい内容が別々のテーブルにあるためテーブルを結合しなければいけません。

SQLのレッスンでもやったjoinを使っていきます。

忘れている場合はSQLのレッスンを見直してみましょう。

memoのuser_idとusersのidを結合して1つのテーブルにして取得します。

これをLaravelのeloquentで書くと以下のようになります。

Memo::select(
    'memo.id',
    'memo.content',
    'memo.created_at',
    'memo.user_id',
    'users.name'
)->join('users', 'memo.user_id', '=', 'users.id')
->where('invalid', 0)
->get();

joinメソッドですが、

join(’結合先テーブル名’, ‘結合元テーブルのキー’, ‘=’, ‘結合元テーブルのキー’)になります。

今回は「memo」テーブルに「users」を結合します。

結合先テーブル名は「users」

結合先テーブルのキーは「memo」テーブルのuser_id

結合元テーブルのキーは「users」テーブルのid

当てはめるとjoin('users', 'memo.user_id', '=', 'users.id')になります。

このままコントローラに書いてもいいですが、少しコード数が多くなってしまったので、この部分はモデルに書いていきましょう。

コントローラはあくまで、情報を受け取って情報を返すだけのファイルです。

本来であればモデルやレポジトリに書くのが適切です。

「Memo.php」に以下のメソッドを追記しましょう。

public static function getUserMemoInfo()
    {
        return Memo::select(
            'memo.id',
            'memo.content',
            'memo.created_at',
            'memo.user_id',
            'users.name'
        )->join('users', 'users.id', '=', 'memo.user_id')
            ->where('invalid', 0)
            ->get();
}

このモデルに書いたメソッドをコントローラで呼び出してみましょう。

TimeLineController.php のshowメソッドを以下のように追記してください。

use App\\Models\\Memo; // 追記
    public function show() {
        $memo_info = Memo::getUserMemoInfo(); // 追記
        return view('timeline')
            ->with('memo_info', $memo_info); // 追記
    }

Memo::getUserMemoInfo();とすることでMemoモデルのgetUserMemoInfoメソッドを呼び出すことができます。

getUserMemoInfoメソッドはstaticメソッドなのでMemo::getUserMemoInfo();で呼び出すことができます。staticがなければMemo::getUserMemoInfo();ではエラーが出てしまいます。

以下のように記述しないようにいけなくなります。

$memo = new Memo();
$memo_info = $memo->getUserMemoInfo();

上記のようにインスタンス化した後に呼び出さなくてはいけません。

ブレードファイルも追記します。

timeline.blade.php

@extends('layouts.base') @section('content') 
<div class="memo_area">
  <div class="memo_show">@foreach($memo_info as $memo) 
    <div class="memo_item">
      <div class="memo_title">
        <time>ユーザー名:{{ $memo->name }} {{$memo->created_at}}</time>
        <p>{{$memo->content}}</p>
      </div>
      <div class="btn_area">
        <div class="edit_form">
          <form action="{{ asset('/edit/'.$memo->id) }}" method="get">@csrf 
            <input type="submit" value="編集">
          </form>
        </div>
        <div class="del_area">
          <form action="{{ asset('/delete') }}" method="post">@csrf 
            <input type="hidden" name="delete_id" value="{{$memo->id}}">
            <input type="submit" value="削除">
          </form>
        </div>
      </div>
    </div>@endforeach 
  </div>
</div>@endsection

ページを読み込んでみましょう。

上記のように、他のユーザーのメモも表示されたらOKです。

ユーザー名も正しいか確認しましょう。

今のままでは他のユーザーのメモも編集できるようになっているので、他のユーザの場合は編集・削除ボタンが非表示になるように修正しましょう。

TimeLineContorller.php の showメソッド

public function show() {
    $memo_info = Memo::getUserMemoInfo();
    $current_user_id = Auth::id(); // 追記
    return view('timeline')
        ->with('current_user_id', $current_user_id) // 追記
        ->with('memo_info', $memo_info);
}

現在ログイン中のユーザーIDを取得しています。

timeline.blade.php

@foreach($memo_info as $memo) 
<div class="memo_item">
  <div class="memo_title">
    <time>ユーザー名:{{ $memo->name }} {{$memo->created_at}}</time>
    <p>{{$memo->content}}</p>
  </div>@if($current_user_id === $memo->user_id) // 追記 
  <div class="btn_area">
    <div class="edit_form">
      <form action="{{ asset('/edit/'.$memo->id) }}" method="get">@csrf 
        <input type="submit" value="編集">
      </form>
    </div>
    <div class="del_area">
      <form action="{{ asset('/delete') }}" method="post">@csrf 
        <input type="hidden" name="delete_id" value="{{$memo->id}}">
        <input type="submit" value="削除">
      </form>
    </div>
  </div>@endif // 追記 
</div>@endforeach

ページを読み込んでみましょう。

無事に編集・削除ボタンが非表示になりました!

@if($current_user_id === $memo->user_id)で現在のユーザーとメモを書いたユーザーを比べています。一致したときだけ表示されます。

これでタイムライン画面が完成しました。

認可について

認証されたユーザーが特定のリソースに対して更新などの処理を許可することを認可といいます。先ほど「現在のユーザーとメモを書いたユーザーが一致した場合削除できる」というのも認可になります。

先ほどの例はかなり分かりやすいですが、もしユーザーに「有償ユーザー」や「管理者ユーザー」といったタイプが出てきた場合どうなるでしょうか。例えば管理ユーザーと有償の投稿ユーザーが削除できるようにしたい場合、先ほどの記載方法だと

/**
* $current_user_typeに
*「管理者(admin)」「有償ユーザ(billing)」「一般(general)」
* のどれかが入っているとします
*/

// 管理者ユーザーと有償ユーザーのみが削除できる
@if(($current_user_id === $memo->user_id && $current_user_type === "billing")
|| $current_user_type === "admin")

複雑な条件分岐をそのまま書くことになります。さらに「投稿から24時間以内なら削除できる」といった条件などを追加していくとさらに複雑になってきます。これらの認可を色々な機能で実施する場合、想定していない操作が行われたりしてしまいます。また、変更する際にはかなりの労力がかかってしまいます。

そこでLaravelにある「ポリシー」という認可を管理するクラスを利用してみましょう。ポリシーは特定の動作に対する認可ロジックを共通化することができます。

ポリシーを作成する

~~内容考え中~~

24ユーザープロフィール画面を作成する

完成形の画面イメージは以下です。

Twitterのような、フォロー、フォロワー数や、ユーザのメモ内容が見られるページになっています。

このページにはタイムラインのユーザー名の部分をリンクにし、リンクをクリックするとこのページに飛ぶようにします。

フォロー機能を実装するにあたりテーブルを1つ作成します。

「フォローしている/フォローされている」の関係は、多対多の関係です。

また、最終課題である「お気に入り機能」も、多対多の関係です。

1対多・多対多の関係は必ず復習してください。

usersとusersのフォロー関係のレコードを保存する中間テーブル「follow」を作成しましょう。

public function up(): void {
    Schema::create('follow', function (Blueprint $table) {
        $table->id();
        $table->integer('user_id')->nullable(false);
        $table->integer('follow_id')->nullable(false);
        $table->integer('invalid')->default(0);
        $table->timestamps();
    });
}

カラムは大きく「user_id」「follow_id」になります。

user_idが2、follow_idが1であるとき、usersのIDが2であるユーザーがusersのIDが1であるユーザーをフォローしているという意味になります。

逆を言うとusersのIDが2であるユーザーはusersのIDが1であるユーザーにフォローされているという意味になります。

つまり、usersのIDが1であるユーザーのフォロー数を数えたければ、「follow」テーブルのuser_idが1であるレコード数を取得するとユーザーのフォロー数を取得することができます。

フォロワー数を数えたければ、「follow」テーブルのfollow_idが1であるレコード数を数えるとフォロワー数をカウントすることができます。

それぞれをコードで表せと以下のようになります。

// フォロー数を取得
Follow::where('user_id', $user_id)->where('invalid', 0)->count();

// フォロワー数を取得
Follow::where('follow_id', $user_id)->where('invalid', 0)->count();

count()メソッドでレコード数を取得することができます。

これでフォロー・フォロワー数を取得することができました。

では実際にページを作成していきましょう。

timeline.blade.php

<div class="memo_show">@foreach($memo_info as $memo) 
<div class="memo_item">
<div class="memo_title">
<time>
  <a href="/user_profile/{{$memo->user_id}}">// 追記 ユーザー名:{{ $memo->name }} </a>{{$memo->created_at}}
</time>
<p>{{$memo->content}}</p>
</div>

コントローラを作成

php artisan make:controller UserProfileController

UserProfileController.php

public function show($user_id) {
    // メモ一覧を取得
    $memo_info = Memo::where('user_id', $user_id)->where('invalid', 0)->get();
    // 対象のユーザー情報を取得
    $user_info = User::find($user_id);

    return view('user_profile')
        ->with('memo_info', $memo_info)
        ->with('user_info', $user_info);
}

web.php

Route::get('/user_profile/{user_id}',
'App\\Http\\Controllers\\UserProfileController@show');

user_profile.blade.php

@extends('layouts.base') @section('content') 
<div class="user_profile_area"> 
  <form action="/follow/{{$user_info->id}}" method="post">@csrf 
    <input class="follow-btn" type="submit" value="フォローする">
  </form>
  <h2>ユーザー名:{{$user_info->name}}</h2>
  <ul class="profile_list">
    <li>フォロー数:0</li>
    <li>フォロワー数:0</li>
    <li>お気に入り数:0</li>
  </ul>
  <div class="memo_show" style="margin: auto">@foreach($memo_info as $memo) 
    <div class="memo_item">
      <div class="memo_title">
        <time>{{$memo->created_at}}</time>
        <p>{{$memo->content}}</p>
      </div>@if($user_info->id === $current_user_id) 
      <div class="btn_area">
        <div class="edit_form">
          <form action="{{ asset('/edit/'.$memo->id) }}" method="get">@csrf 
            <input type="submit" value="編集">
          </form>
        </div>
        <div class="del_area">
          <form action="{{ asset('/delete') }}" method="post">@csrf 
            <input type="hidden" name="delete_id" value="{{$memo->id}}">
            <input type="submit" value="削除">
          </form>
        </div>
      </div>@endif 
    </div>@endforeach 
  </div>
</div>@endsection

style.css(追記)

.follow-btn {
    border: #1da1f2 solid 3px;
    padding: 5px 10px;
    border-radius: 3px;
    background-color: transparent;
    font-weight: bold;
}
.unfollow-btn {
    border: #1da1f2 solid 3px;
    padding: 5px 10px;
    border-radius: 3px;
    background-color: #1da1f2;
    color: #fff;
    font-weight: bold;
}

ではページを読み込んで「タイムラインページ」を開いてください。

メモのユーザー名の部分がリンクになったとおもいます。

クリックしてみましょう。

クリックしたユーザーのページに遷移できました。

クリックしたユーザーのメモを表示することができています。

メモ取得ロジックは前回と同じです。

では残りの

  1. フォロー機能の実装
  2. フォローボタンの切替
  3. フォロー解除機能の実装
  4. フォロー数の表示
  5. フォロワー数の表示

を実装してきましょう。

1.フォロー機能の実装

すでにURLは設定してあります。

user_profile.blade.php

<div class="user_profile_area"> 
<form action="/follow/{{$user_info->id}}" method="post">@csrf 
  <input class="follow-btn" type="submit" value="フォローする">
</form>

この部分でフォローするをクリックすると「/follow/user_id」に遷移します。

ここでのユーザーIDはログインしているユーザーのIDと一致するとは限りません。

UserProfileController.phpにメソッドを追記していきます。

public function followUser($follow_id) {
    $user_id = Auth::id();
    Follow::create(
        [
            'user_id' => $user_id,
            'follow_id' => $follow_id
        ]
    );
    return redirect('/user_profile/' . $follow_id);
}

$followでフォロー対象のusersのIDを取得しています。

その後、現在ログイン中のユーザーのIDを取得し「follow」テーブルに保存しています。

では実際にfollowUserメソッドが実行されるようにweb.phpに追記していきましょう。

Route::post('/follow/{follow_id}',
'App\\Http\\Controllers\\UserProfileController@followUser');

これでフォロー機能が実装出来ました。

それでは実際にフォローボタンをクリックしDBに保存されているか確認していきましょう。

ちゃんと入っていますね!

実際の数値は皆さんと違うと思います。

画面を見てみましょう。

DBに値は追加されていますがボタンはまだ「フォローする」になっています。

次はこれを切り替えられるようにしましょう。

2.フォローボタンの切替

user_profile.blade.phpを編集していきます。

@if($is_follow) 
<form action="/unfollow/{{$user_info->id}}" method="post">@csrf 
  <input class="unfollow-btn" type="submit" value="フォローを外す">
</form>@else 
<form action="/follow/{{$user_info->id}}" method="post">@csrf 
  <input class="follow-btn" type="submit" value="フォローする">
</form>@endif

UserProfileController.php

public function show($user_id) {
    // メモ情報を取得
    $memo_info = Memo::where('user_id', $user_id)->where('invalid', 0)->get();
    
    // ユーザー情報を取得
    $user_info = User::find($user_id);

    // 現在のユーザーIDを取得
    $current_user_id = Auth::id();

    // フォローしているかどうかを判別
    $is_follow = Follow::where('user_id', $current_user_id)
        ->where('follow_id', $user_id)
        ->where('invalid', 0)->exists();

    return view('user_profile')
        ->with('memo_info', $memo_info)
        ->with('is_follow', $is_follow)
        ->with('current_user_id', $current_user_id)
        ->with('user_info', $user_info);
}

注目すべきは$is_follow = Follow::where('user_id', $current_user_id)->where('follow_id', $user_id)->where('invalid', 0)->exists();この部分です。

最後のexists()ですが対象のレコードが存在するかどうかを判別します。

存在する場合は「true」存在しない場合は「false」が返り値となります。

users_idがログインしているユーザのIDでfollower_idが選択されたメモのユーザーIDであるレコード、つまり、ログイン中のユーザーが選択したメモのユーザーをフォローしているかどうかは判断します。

存在していた場合はtrue 存在しなかった場合はfalseになります。

ではページを読み込んでみましょう。

無事にフォローを外すボタンが表示されました。

次はフォロー解除機能を作っていきましょう。

3.フォロー解除機能の実装

user_profile.blade.phpにすでに遷移先URLは設定しています。

@if($is_follow) 
<form action="/unfollow/{{$user_info->id}}" method="post">@csrf 
  <input class="unfollow-btn" type="submit" value="フォローを外す">
</form>@else 
<form action="/follow/{{$user_info->id}}" method="post">@csrf 
  <input class="follow-btn" type="submit" value="フォローする">
</form>@endif

フォローを外すボタンをクリックすると「/unfollow/user_id」に遷移するようになっています。

ではUserProfileController.phpに追記しましょう。

public function unfollowUser($follow_id) {
    $user_id = Auth::id();
    Follow::where('user_id', $user_id)
        ->where('follow_id', $follow_id)
        ->update(['invalid' => 1]);
     
    return redirect('/user_profile/' . $follow_id);
}

一致するレコードの「invalid」を1に上書きして論理削除しています。

画面を読み込んで、フォロー解除ボタンをクリックしましょう。

ボタンが「フォローする」に変化しています。

DBを確認します。

無事に「invalid」に1が入っています。

これでフォロー削除機能が実装出来ました。

では続いてフォロー数を画面に表示できるように実装していきましょう。

4.フォロー数の表示

フォロー数を取得する処理は先ほど紹介しました。

// フォロー数を取得
Follow::where('user_id', $user_id)->where('invalid', 0)->count();

これでフォロー数を取得することができます。

ではこれを踏まえてメソッドに追記していきましょう。

public function show($user_id) {
    // メモ情報を取得
    $memo_info = Memo::where('user_id', $user_id)->where('invalid', 0)->get();
    
    // ユーザー情報を取得
    $user_info = User::find($user_id);

    // 現在のユーザーIDを取得
    $current_user_id = Auth::id();

    // フォロー数を取得
    $follow_count = Follow::where('user_id', $user_id)->where('invalid', 0)->count();
  
    // フォローしているかどうかを判別
    $is_follow = Follow::where('user_id', $current_user_id)
        ->where('follow_id', $user_id)
        ->where('invalid', 0)->exists();

    return view('user_profile')
        ->with('memo_info', $memo_info)
        ->with('follow_count', $follow_count)
        ->with('is_follow', $is_follow)
        ->with('current_user_id', $current_user_id)
            ->with('user_info', $user_info);
}

user_profile.blade.phpに追記します。

<ul class="profile_list">
  <li>フォロー数:{{$follow_count}}</li>// 追記
  <li>フォロワー数:0</li>
  <li>お気に入り数:0</li>
</ul>

ユーザーをフォローした後ログイン中のアカウントを開いてみましょう。

※ログイン中のユーザーのメモがないとログイン中ユーザー画面に遷移することはできません

フォロー数が1になっていますね。

これでフォロー数表示部分が実装出来ました。

ログイン中のユーザーの画面を開くときはフォローボタンを非表示にする処理を追加しておきましょう。

user_profile.blade.phpに追記してください。

@if($user_info->id !== $current_user_id) // 追記 @if($is_follow) 
<form action="/unfollow/{{$user_info->id}}" method="post">@csrf 
  <input class="unfollow-btn" type="submit" value="フォローを外す">
</form>@else 
<form action="/follow/{{$user_info->id}}" method="post">@csrf 
  <input class="follow-btn" type="submit" value="フォローする">
</form>@endif @endif // 追記

$user_info->idにはメモを記載したユーザーのIDが入っています。

$current_user_idには現在ログイン中のユーザーIDが入っています。

$user_info->id$current_user_idが一致しない時だけボタンを表示します。

一致する場合は自分の画面になるのでボタンを非表示にします。

最後にフォロワー数表示部分を作成していきます。

5.フォロワー数の表示

フォロワー数を取得する処理は先ほど作成した、

// フォロワー数を取得
Follow::where('follow_id', $user_id)->where('invalid', 0)->count();

です。

ではこれを踏まえてUserProfileController.phpに追記します。

public function show($user_id) {
    // メモ情報を取得
    $memo_info = Memo::where('user_id', $user_id)->where('invalid', 0)->get();
    
    // ユーザー情報を取得
    $user_info = User::find($user_id);
    
    // 現在のユーザーIDを取得
    $current_user_id = Auth::id();

    // フォロー数を取得
    $follow_count = Follow::where('user_id', $user_id)->where('invalid', 0)->count();

    // フォロワー数を取得
    $followed_count = Follow::where('follow_id', $user_id)->where('invalid', 0)->count();

    // フォローしているかどうかを判別
    $is_follow = Follow::where('user_id', $current_user_id)
        ->where('follow_id', $user_id)
        ->where('invalid', 0)->exists();

    return view('user_profile')
        ->with('memo_info', $memo_info)
        ->with('follow_count', $follow_count)
        ->with('followed_count', $followed_count)
        ->with('is_follow', $is_follow)
        ->with('current_user_id', $current_user_id)
        ->with('user_info', $user_info);
}

user_profile.blade.phpを追記します

<ul class="profile_list">
  <li>フォロー数:{{$follow_count}}</li>
  <li>フォロワー数:{{$followed_count}}</li>
  <li>お気に入り数:0</li>
</ul>

ではフォローしたユーザーを表示しましょう。

無事にフォロワー数が1になっています。

ここまでの内容をGitで管理するようにしましょう。

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

// 初期化
git init

git add .
git commit -m "メモ共有機能"

// デフォルトのmasterブランチをmainブランチに名前を変更
git branch -M main

また、今回の共有機能用リモートリポジトリをGithub上に作成しましょう。

ここでも、リモートリポジトリの作成方法とpushの方法は、Gitの教材を参考にしてください。

お疲れ様でした!以上でメモ共有機能は完成です

最後に課題があるので考えてみましょう。

RESTfulAPIの開発

APIとは

RESTfulAPIとは

RESTfulAPIとは、特定の原則・規則性を持ったAPIを指します。

~~内容考え中~~

そのためRESTfulAPIでは必要以上にパスの種類を増やさず、HTTPメソッドを使い分けることでそれぞれの機能が使えるようにします。

いままでのルートの記載方法

// 取得
Route::get('/', 'App\\Http\\Controllers\\MemoController@show');
// 登録
Route::post('/add', 'App\\Http\\Controllers\\MemoController@add');
// 削除
Route::post('/delete', 'App\\Http\\Controllers\\MemoController@delete');
// 更新
Route::post('/edit/{memo_id}', 'App\\Http\\Controllers\\MemoController@edit');

RESTfulAPI

// 取得
Route::get('/memo', 'App\\Http\\Controllers\\MemoController@get');
// 登録
Route::post('/memo', 'App\\Http\\Controllers\\MemoController@insert');
// 削除
Route::delete('/memo/{memo_id}', 'App\\Http\\Controllers\\MemoController@delete');
// 更新
Route::put('/memo/{memo_id}', 'App\\Http\\Controllers\\MemoController@edit');

APIを作成してみよう

今回はCRUDの機能をそれぞれ作成してみます。

routes/api.php


次にコントローラを作成します。


コントローラの中身を記載していきます。

それではAPIにリクエストを送ってみましょう。

テストとデバッグ

テストについて

ここまでメモアプリを作成してきました。これで完成……と行きたいところですが、その前に全ての機能が正常に動くかを確認する必要があります。

テストは先ほどまでのように実際に人が動かしながら動作を確認する手動テストだけでなく、プログラムに任せる自動テストがあります。

laravelにおける自動テストは大きく分けると「ユニットテスト」「フィーチャーテスト」「ブラウザテスト」の3つですが、今回は「ユニットテスト」と「フィーチャーテスト」の書き方を学んでいきます。

テスト前の準備

テストを作成する前に準備をしていきましょう。

テストでは開発環境のデータに影響されないよう、別のDB等に接続してテストします。

(のちに説明しますが、テスト中にデータが追加されたり、テーブルのデータを削除したりもします。)

毎回接続先を変えるわけにもいかないので、テストの時に使う環境変数を設定しておき、テストの際にはそちらを読み込むようにします。

今回はDBのスキーマをもう一つ作り、そこをテスト用に動作させます。MySqlに接続し、スキーマを作成しましょう。

CREATE SCHEMA `laravel_test` ;

次にテスト用の環境変数ファイルを作成します。

.envファイルを複製し、ファイル名を「.env.testing」に変更します。

.env.testingの中身のAPP_ENVとDB_DATABASEをそれぞれ

APP_ENV=test
DB_DATABASE=laravel_test

に変更します。

次に先ほど作成したスキーマにマイグレーションを実行します。

php artisan migrate --env=testing 

–envオプションで使用する環境変数を指定することができます。

それではテストできる環境が整ったので、既にあるテストを動かしてみましょう。artisanコマンドでテストを実行することが出来ます。

php artisan test

実行するといくつか結果が出てきたと思います。

まずトータルの結果を確認していきます。

(結果の値が違うかもしれませんが、気にしないでください)

1つのテスト関数につき1つのテストとします。成功しているテストがpassed、失敗している場合はfaliedで表示されます。assertionsはテスト内で実行している(このあと説明する)アサートの数を指しています。

次に失敗したテストの内容を確認します。

とあります。このテストコードは「’/’にアクセスし、正常にレスポンスが返ってきたか」を確認しています。

get関数で指定したルートへの(疑似的な)リクエストを行ないます。

そしてassertStatus関数でレスポンスのステータスを確認します。

アサート(assert)はテストが期待通りに動作していることを検証するために使用される関数です。アサート関数については種類がいくつかあるので、時間があれば調べてみてください。

~で学んだようにレスポンスのステータスが200の場合は正常に返ってきていることを示しているので、「正常に遷移できたかどうか」をテストするコードとなります。

しかし、実際には302(リダイレクト)が返ってきているためエラーとなっています。

先ほどまでで作成したroutes/web.phpを確認してみると

routes/web.php

Auth::routes(); Route::group(['middleware' => 'auth'], function () {
    Route::get('/', 'App\\Http\\Controllers\\MemoController@show');
    Route::post('/add', 'App\\Http\\Controllers\\MemoController@add');
    Route::post('/delete', 'App\\Http\\Controllers\\MemoController@delete');
    Route::get('edit/{edit_id}', 'App\\Http\\Controllers\\MemoController@getEdit');
    Route::post('update', 'App\\Http\\Controllers\\MemoController@postEdit');
});

‘/’にアクセスする際にログインしていることを前提としています。

そのため、ログインせずにリクエストするとログイン画面にリダイレクトされてしまっているのでエラーとなっています。

あくまでも「テスト作成時に想定されていた内容」でテストを実行するため、テストを修正すべきなのか、コードを修正するべきなのかは考える必要があります。

このようにコマンドひとつでテストを実行することが出来ます。

ユニットテスト作成

まずユニットテストを作成してみましょう。ユニットテストとは、単体テストとも呼ばれ小さな機能ひとつひとつを確認するテストになります。

php artisan make:test CommonTest --unit

php artinsan make:test

にするとtinkerみたいに対話型で作成してくれます。名前を指定するとオプションがない限り強制でFeatureテストが作られます。

作成したテストはtestsフォルダに格納されます。

ユニットテストの特徴として動作が早いものの、Laravelアプリケーションを起動起動しないためDBをつかった関数等がテスト出来ない等の制限があります。

共通ロジック等を作った場合はこちらでテストを作成します。

フィーチャーテストの作成

laravelで主に使われるのは「フィーチャーテスト」です。こちらは機能テストとも呼ばれます。urlへのアクセスから表示までをテストすることが出来ます。

それではフィーチャーテストを新規で作成してみましょう。

php artinsan make:test MemoControllerTest

作成したテストはtests/Featureフォルダに格納されます。

先ほど例に出したようなテストが生成されているはずです。ただ、このまま実行すると先ほどのようにエラーになってしまいます。

今回はログイン後の動作確認をするため「ユーザーを作成する→ログインした状態にする」コードを書きます。

public function test_example(): void
    {
        $user = User::factory()->create(); // 追記
        $response = $this->actingAs($user)->get('/'); // 修正

        $response->assertStatus(200);
    }

まずモデルのファクトリーメソッドを使いユーザーデータを生成します。

actingAsメソッドではテスト上でログイン状態を再現する関数です。この関数を用いてログイン状態にした後リクエストを送ります。

それでは作成したテストを動かしてみましょう。今回は作成したテストだけ実施したいのでファイルを指定します。

php artisan test tests/Feature/MemoControllerTest.php

OKになりました。

ではもう一つ関数を足してテストしてみましょう。

public function test_add(): void

    {

        $user = User::factory()->create();

        $response = $this->actingAs($user)->post('/add',[

            'memo_text' => 'test memo',

        ]);

        // リダイレクト先を確認

        $response->assertStatus(302)

        ->assertRedirect('/');

        // 追加したメモがDBに保存されているか確認

        $this->assertDatabaseHas('memos', [

            'user_id' => $user->id,

            'content' => 'test memo',

        ]);

        // ホームに追加したメモが表示されているか確認

        $this->get('/')->assertSeeText('test memo');

    }

このテスト関数では「メモ追加」のテストを行っています。

ログイン後にリクエストメッセージ「memo_text」に’test memo’を入れてリクエストを送ります。リクエスト自体は登録処理後にリダイレクトするのでステータスコードが302かどうかを検証します。

assertDatabaseHas関数でDBに登録されているか、再度遷移しassertSeeText関数で’test memo’が表示されているかを検証しています。

さて、ここで何度可実行してみてDBを確認してみると

use laravel_test;
SELECT count(*) FROM memos;

データが増えていることが分かります。これはテスト中にユーザーデータやメモを作成しているためです。

例えばタイムライン画面のテストを作る際、適当にデータを入れて全て表示しているか確認しようとすると、毎回表示される数が異なったりと正しいテストが出来ません。

そのためにテストケースごとにDBをリセットさせてしまいましょう。(テスト内でユーザーを毎回作っていたのはそのためです)

class MemoControllerTest extends TestCase
{
    use RefreshDatabase; // 追記

※注意※これを設定して実行するとDB上のデータが全て消えてしまいます。テスト作成時に不安な場合はこれをコメントアウトした状態でテストファイルを指定して実行し、想定したDBやスキーマ等とつながっているか確認しましょう。

もう一度実行してみます。

php artisan test tests/Feature/MemoControllerTest.php

実行結果を確認しDBを確認するとデータが全て消えていることが分かります。

use laravel_test;
SELECT count(*) FROM memos;

laravelではこのようにテストを作成していきます。

他にも色々なアサートがあるので余裕があれば確認してみましょう。

また今回は触れませんでしたが、Pestを使ったテストやブラウザテストを実施するDusk等もあるので、今はまだテストに興味が持てないかもしれませんが、余裕が出来たら学習してみましょう。

25課題

1.フォロー機能

フォローされると「follow」テーブルにレコードがインサートされ、フォローが外れると、「follow」テーブルのinvalidが1に上書きされ論理削除されます。

このままでは、フォローボタンを連打されると無駄なデータがたくさん作成されてしまいます。

そこで、フォローボタンが連打されたとしてもレコードが1件だけしか作成されないようにロジックを変更してください。

2.お気に入り機能を実装してみよう。

以下の要件を満たすようにしてください。

  • 自分を含むすべてのユーザーのメモをお気に入りできるようにする
  • タイムラインでのみお気に入り登録できるようにする
  • お気に入り数を画面に表示する
  • テーブルやコントローラなどは自由に作成してください。
  • ただし作成するテーブル名は「favorite」にしてください。

お気に入りボタンについては以下のHTMLとCSSを使用してください。

timeline.blade.php

<div class="memo_item">
<div class="memo_title">
<time>  
  <a href="/user_profile/{{$memo->user_id}}">ユーザー名:{{ $memo->name }}</a>{{$memo->created_at}}
</time>
<p>{{$memo->content}}</p>
</div>// ここから追記 
<div class="favorite">
<form action="">
  <input type="submit" value="♡">
</form>
<form action="">
  <input type="submit" value="♥">
</form>
</div>// ここまで

style.css

.favorite {
    margin: auto 5px;
}
.favorite form input {
    border: none;
    background: transparent;
    padding: 5px;
}

イメージ

お気に入り未登録時

お気に入り登録時

3.オリジナルサービスを作ろう


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です