Laravel5.7: postsとusersを関連させる


記事のテーブルにユーザーのIDを外部キーとして追加することで、記事の著者が表示されるようにします。

親記事

Laravel 5.7で基本的なCRUDを作る - Qiita

モデルにリレーションを追加する

readouble.com: リレーション

モデルにメソッドを追加します。
それぞれのメソッド名の単数形と複数形に注意してください。

なおUserモデルでは、記事を新しい順で取得するためにlatest()を使っています。
readouble.com: latest/oldest

app/User.php
    /**
     * リレーション (1対多の関係)
     *
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
     */
    public function posts() // 複数形
    {
        // 記事を新しい順で取得する
        return $this->hasMany('App\Post')->latest();
    }
app/Post.php
    /**
     * リレーション (従属の関係)
     *
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
     */
    public function user() // 単数形
    {
        return $this->belongsTo('App\User');
    }

なお、戻り値の型はデバッグバーの\Debugbar::info()で調べました。

postsのマイグレーションに外部キーを追加する

readouble.com: 外部キー制約

外部キー用のuser_idカラムをpostsテーブルに追加します。
その際、usersテーブルでレコードが削除されたら、postsテーブルで関連しているレコードも削除するようにします。
つまり、会員が削除されたらその人の記事も削除するということです。

また、user_idのデフォルト値を1にしています。
こうすると、新たに作る記事の著者がID1番のユーザーに固定されてしまいます。
後で実装するログイン機能によって、著者のIDが正しく保存されるように変更します。

database/migrations/2017_05_22_041557_create_posts_table.php
     public function up()
     {
         Schema::create('posts', function (Blueprint $table) {
             (中略)
+            $table->integer('user_id')->unsigned()->default(1);
+            $table->foreign('user_id')
+                  ->references('id')->on('users')
+                  ->onDelete('cascade');
         });
     }

postsのシーダーにもuser_idを追加して、著者としてのユーザーIDをランダムに割り当てます。
usersのシーダーでは20人のユーザーを作っているので、数値の範囲は1~20とします。
Faker

database/seeds/PostsTableSeeder.php
     DB::table('posts')->insert([
         (中略)
+        'user_id' => $faker->numberBetween(1, 20),
     ]);

テーブル構造が変わってしまったので、マイグレーションとシーディングをやり直すのを忘れないでください。

PowerShell
> php artisan migrate:refresh --seed

postsのビューを修正する

001.png

{{ $post->user->name }}のようにして記事の投稿者の情報を表示できます。
なお、コントローラは何も修正しません。

resources/views/posts/index.blade.php
         <table class="table table-striped">
             <thead>
                 <tr>
+                    <th>{{ __('Author') }}</th>
                     <th>{{ __('Title') }}</th>

(中略)

             @foreach ($posts as $post)
                 <tr>
+                    <td>
+                        <a href="{{ url('users/' . $post->user->id) }}">
+                            {{ $post->user->name }}
+                        </a>
+                    </td>
resources/views/posts/show.blade.php
 <dl class="row">
+        <dt class="col-md-2">{{ __('Author') }}:</dt>
+        <dd class="col-md-10">
+            <a href="{{ url('users/' . $post->user->id) }}">
+                {{ $post->user->name }}
+            </a>
+        </dd>

usersのビューを修正する

{{ $post->title }}のようにしてユーザーが投稿した記事を表示できます。
ユーザーのshowページで、その人が書いた記事を新しい順に表示することにします。
下記を丸ごと、Bootstrapの.containerの終了タグの直前に追加してください。

resources/views/users/show.blade.php
    {{-- ユーザーの記事一覧 --}}
    <h2>{{ __('Posts') }}</h2>
    <div class="table-responsive">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th>{{ __('Title') }}</th>
                    <th>{{ __('Body') }}</th>
                    <th>{{ __('Created') }}</th>
                    <th>{{ __('Updated') }}</th>

                    {{-- 記事の編集・削除ボタンのカラム --}}
                    <th></th>
                </tr>
            </thead>
            <tbody>
                @foreach ($user->posts as $post)
                    <tr>
                        <td>
                            <a href="{{ url('posts/' . $post->id) }}">
                                {{ $post->title }}
                            </a>
                        </td>
                        <td>{{ $post->body }}</td>
                        <td>{{ $post->created_at }}</td>
                        <td>{{ $post->updated_at }}</td>
                        <td nowrap>
                            <a href="{{ url('posts/' . $post->id . '/edit') }}" class="btn btn-primary">
                                {{ __('Edit') }}
                            </a>
                            @component('components.btn-del')
                                @slot('table', 'posts')
                                @slot('id', $post->id)
                            @endcomponent
                        </td>
                     </tr>
                @endforeach
            </tbody>
        </table>
    </div>
    {{ $user->posts->links() }}

hasManyのページング

上のビューで、ページリンクの{{ $user->posts->links() }}を動作させるため、Userコントローラに追記が必要です。

app/Http/Controllers/UserController.php
     public function show(User $user)
     {
+        // そのユーザーが投稿した記事のうち、最新5件を取得
+        $user->posts = $user->posts()->paginate(5);
         return view('users.show', ['user' => $user]);
     }

(補足) SQLiteで外部キー制約を有効にする

※ データベースにSQLiteを使わない場合は、この項目は読み飛ばしてください。
この項目はLaravel5.4以来手を入れておらず、古い情報です。

SQLiteで外部キー制約を使えるようにする - ..たれろぐ..
SQLiteで外部キー制約(Foreign Key)が効かない時 – ララ帳

SQLiteでは、カラムにonDelete('cascade')を追加しただけではユーザーを削除してもそのユーザーが投稿した記事を自動で削除してはくれません。
DBに接続するたびにPRAGMA foreign_keys = ON;を実行しなければなりません。
これをLaravelで行うには、app/Providers/AppServiceProvider.phpboot()に下記を追加します。

app/Providers/AppServiceProvider.php
public function boot()
{
    if (\DB::getDriverName() == 'sqlite') {
        \DB::statement(\DB::raw('PRAGMA foreign_keys=1'));
    }
}

ウェブ上の多くの情報では、DBがSQLiteかどうかを判断するために下記のIF条件を使っています。

// 私の環境では正常に分岐しない
if (\DB::connection() instanceof \Illuminate\Database\SQLiteConnection) {

これでもいいのですが、私の環境ではなぜか初めは正常に動作しませんでした。
php artisan tinkervar_dump()\DB::connection() instanceof \Illuminate\Database\SQLiteConnectionを確認してもtrueが帰ってきます。
なのにIF文の中身が実行されません。
その後、いろいろいじっていると動くようになったのですが、なぜ解決したのかが分かりません。
私の他にも
同じ悩みを抱える人がいました。
なので、最初に挙げたIF条件を使うことにしました。