Laravelで作られた掲示板に機能を追加する③

[Laravelで作られた掲示板に機能を追加する②](https://qiita.com/YKhojo/items/fd86d0c3d1b2273f310a)の続きです。

## 目標

- アクセス制限を実装
 1. Laravel-permissionのインストール　～　seedの流し込み
 1. viewの調整
 1. 気が付いたエラーの修正

色々と出来そうで触ってみたいという理由から、laravel-permissionを用います。
現時点では、管理者と一般ユーザーだけの制御を考えています。

|Role(役割)|Permission(権限)|備考|
| ---- | ---- | ---- |
|admin|admin_permission, user_permission|すべてのユーザーの記事を変更可能|
|user|user_permission|ログインユーザーの記事を変更可能|


参考サイト
https://qiita.com/Fell/items/7cd398b8ae65ac42950f
基本的なインストールはこのサイトを参考にしています。
seedの流し込みまでは結構端折っているので、一読するのをおすすめします。
エラーが出たところは解決のために記述を増やしているので、サイトと差異のある記述箇所があります。

その他の参考サイト
https://webty.jp/staffblog/production/post-3917/
https://reffect.co.jp/laravel/spatie-laravel-permission-package-to-use
https://econosys-system.com/blog/archives/342

<br>

## Laravel-permissionのインストール　～　seedの流し込み

Laravel-permissionのインストール

```
$ composer require spatie/laravel-permission
```

二か所に追記

```app/Http/Kernel.php
protected $routeMiddleware = [

    'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, // ←　追加
    'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, // ←　追加
    'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, // ←　追加
]
```

```app/Models/User.php
use Spatie\Permission\Traits\HasRoles; // ←　追加

class User extends Authenticatable
{
    use Notifiable,HasRoles; // ←　追加
```

マイグレーションファイルの生成とテーブル作成

```
$ php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations"
$ php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config"
$ php artisan migrate
```

<br>

### DBデータの作成

各シーダーのひな形作成を作り、データを流し込みます。

#### 1. PermissionTableSeeder.php

シーダーのひな形作成

```
$ php artisan make:seeder PermissionTableSeeder
```

admminとuserを作成

```database\seeders\PermissionTableSeeder.php
use Spatie\Permission\Models\Permission; // ←　追記
class PermissionTableSeeder extends Seeder
{
    public function run()
    {
        $permissions = [
            'admin_permission', // ←　全権限用
            'user_permission', // ←　一般権限用
        ];
        foreach ($permissions as $permission) {
            Permission::create(['name' => $permission]);
        }
    }
}
```

seederの実行

```
$ php artisan db:seed --class=PermissionTableSeeder
```

Permissonテーブルにadmin_permissionとuser_permissionが入りったので、興味のある方はDBを確認してみてください。

<br>

#### 2. RoleTableSeeder.php

手順は同じなので説明を省きます。

```
$ php artisan make:seeder RoleTableSeeder
```

```database/seeders/RoleTableSeeder.php
<?php
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role; // ←　追記

class RoleTableSeeder extends Seeder
{
    public function run()
    {
        $roles = [
            'admin',
            'user',
        ];
        foreach ($roles as $role) {
            Role::create(['name' => $role]);
        }
    }
}
```

```
$ php artisan db:seed --class=RoleTableSeeder
```

<br>

### 3. RoleHasPermissionTableSeeder.php

```
$ php artisan make:seeder RoleHasPermissionTableSeeder
```

```database\seeders\RoleHasPermissionTableSeeder.php
<?php
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission; // ←　追記　意味なし？
use Spatie\Permission\Models\Role;

(use Illuminate\Support\Facades\Hash; // ←　追記)
(use App\Models\User; // ←　追記)

class RoleHasPermissionTableSeeder extends Seeder
{
    public function run()
    {
        // admin
        $permissions = [
            'admin_permission',
            'user_permission',
        ];
        $role = Role::findByName('admin');
        $role->givePermissionTo($permissions);

        // user
        $permissions = [
            'user_permission',
        ];
        $role = Role::findByName('user');
        $role->givePermissionTo($permissions);  
    }
}
```

```
$ php artisan db:seed --class=RoleHasPermissionTableSeeder
```

<br>

### 4. UserTableSeeder.php

以前作っていたので、変更になります。

```database\seeders\UsersTableSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash; // ←　追記

class UserTableSeeder extends Seeder
{
    public function run()
    {
        //追加
        // admin
        $user = User::create([
            'name'     => 'web管理責任者',
            'email'    => 'admin@gmail.com',
            'password' => Hash::make('admin'),
        ]);
        $user->assignRole('admin');
        
        // user1
        $user = User::create([
            'name'     => 'ユーザー1',
            'email'    => 'user1@gmail.com',
            'password' => Hash::make('user'),
        ]);
        $user->assignRole('user');
        
        // user2
        $user = User::create([
            'name'     => 'ユーザー2',
            'email'    => 'user2@gmail.com',
            'password' => Hash::make('user'),
        ]);
        $user->assignRole('user');

        
        // user3
        $user = User::create([
            'name'     => 'ユーザー3',
            'email'    => 'user3@gmail.com',
            'password' => Hash::make('user'),
        ]);
        $user->assignRole('user');

        
        // user4
        $user = User::create([
            'name'     => 'ユーザー4',
            'email'    => 'user4@gmail.com',
            'password' => Hash::make('user'),
        ]);
        $user->assignRole('user');
}
```

もしダミーデータのユーザー数が異なる場合は、下記の箇所を修正してください。
理由は、ダミーデータのユーザー数と合わせていないと、整合性が取れずにエラーが出るからです。

```database\factories\PostFactory.php　と　database\factories\CommentFactory.php
    public function definition()
    {
        return [

            'user_id' => $this->faker->numberBetween(1,5), // ← 5となっている箇所を、ダミーデータのユーザー数に変更
        ];
    }
```

seedの読み込みでエラーが出た場合は、気にせずにDatabaseSeeder.phpに進んでください。

```
$ php artisan db:seed --class=UserTableSeeder
```

<br>

### 5. DatabaseSeeder.php


php artisan db:seed --class=なんちゃらでデータを流し込んでいたので、refreshやfreshのことを考え、DataBaseSeeder.phpにも書き込んでおきます。
追記は必ずUsersTableSeederの上にしてください。下に書いた場合は、DBの整合性が取れずにエラーになります。

```database\seeders\DatabaseSeeder.php
    public function run()
    {
        $this->call(PermissionTableSeeder::class); // ←　追記
        $this->call(RoleTableSeeder::class); // ←　追記
        $this->call(RoleHasPermissionTableSeeder::class); // ←　追記
        $this->call(UsersTableSeeder::class);
        $this->call(PostsTableSeeder::class);
    }
```

### (6. php artisan migrate:fresh --seed)

必要に応じて下記コマンドの入力を行ってください。
4. UserTableSeeder.phpの`php artisan db:seed --class=UserTableSeeder`でエラーが出た場合は、migrateをやり直すことでエラーの解決につながるかもしれません。

```
$ php artisan migrate:fresh --seed
とか
$ php artisan migrate:refresh --seed
```

[【Laravel】migrate:refresh と migrate:fresh の違い](https://qiita.com/sskre/items/0bd048744ea2f830682e)

とりあえずこれでLaravel permissionのインストールとダミーデータの流し込みが終わりました。

<br>

---
[メモ]
自分の知識では、アクセス制御というのは、users tableにroleカラムを作り、role_idが1ならadmin, 2ならuserのようにするものだと思っていました。

|id|role_id|
| ---- | ---- | 
|1|1|
|2|2|
|3|2|
|4|2|
|5|2|

なので、どうしてlaravel permissionはusers tableにroleカラムを作らないのにアクセス制御が出来るのか不思議でした。

ポリモーフィックリレーションとかいうことをしてるとのことでした。
https://readouble.com/laravel/8.x/ja/eloquent-relationships.html

---

<br>

## 2. viewの編集

いい方法が思いつかなかったので、if文でhasrole('admin')とhasrole('user')の表示を分けました。
さらにuserの時は、`@if( ( $post->user_id ) === ( Auth::user()->id ) )`でidが一致していると編集と削除ボタンを表示します。

今後の課題として、もし必要になるならば、管理用のページとユーザー用のページを分けるとかになるかもしれません。

```resources\views\bbs\index.blade.php
        @foreach ($posts as $post)
            <tr>
                <td>{{ $post->id }}</td>
                <td>{{ $post->category->name }}</td>
                <td>{{ $post->created_at->format('Y.m.d') }}</td>
                <td>{{ $post->user->name }}</td>
                <td>{{ $post->subject }}</td>
                <td>{!! nl2br(e(Str::limit($post->message, 100))) !!}
                @if ($post->comments->count() >= 1)
                    <p><span class="badge badge-primary">コメント：{{ $post->comments->count() }}件</span></p>
                @endif
                </td>
                <td class="text-nowrap">
                    <p><a href="{{ action('App\Http\Controllers\PostsController@show', $post->id) }}" class="btn btn-primary btn-sm">詳細</a></p>

                    @if (Route::has('login')) // ここから
                        @auth
                            @hasrole('admin')
                                <p><a href="{{ action('App\Http\Controllers\PostsController@edit', $post->id) }}" class="btn btn-info btn-sm">編集</a></p>
                                <p>
                                    <form method="POST" action="{{ action('App\Http\Controllers\PostsController@destroy', $post->id) }}">
                                        @csrf
                                        @method('DELETE')
                                        <button class="btn btn-danger btn-sm">削除</button>
                                          
                                    </form>
                                </p>
                            @endhasrole

                            @hasrole('user')
                                @if( ( $post->user_id ) === ( Auth::user()->id ) )
                                    <p><a href="{{ action('App\Http\Controllers\PostsController@edit', $post->id) }}" class="btn btn-info btn-sm">編集</a></p>
                                    <p>
                                        <form method="POST" action="{{ action('App\Http\Controllers\PostsController@destroy', $post->id) }}">
                                            @csrf
                                            @method('DELETE')
                                            <button class="btn btn-danger btn-sm">削除</button>
                                        </form>
                                    </p>
                                @endif
                            @endhasrole
                        @endauth
                    @endif // ここまで

                </td>
            </tr>
        @endforeach
```

編集・編集ボタンのところを分岐させます

```resources\views\bbs\show.blade.php
         <!-- 編集・編集ボタン -->
        @if (Route::has('login'))
            @auth
            @hasrole('admin')
                <div class="mb-4 text-right">
                    <a href="{{ action('App\Http\Controllers\PostsController@edit', $post->id) }}" class="btn btn-info">
                    編集する
                    </a>
                
                    <form
                        style="display: inline-block;"
                        method="POST"
                        action="{{ action('App\Http\Controllers\PostsController@destroy', $post->id) }}"
                    >
                        @csrf
                        @method('DELETE')
                        
                        <button class="btn btn-danger">削除する</button>
                    </form>
                </div>
            @endhasrole

            @hasrole('user')
            @if( ( $post->user_id ) === ( Auth::user()->id ) )
                <div class="mb-4 text-right">
                    <a href="{{ action('App\Http\Controllers\PostsController@edit', $post->id) }}" class="btn btn-info">
                    編集する
                    </a>
                
                    <form
                        style="display: inline-block;"
                        method="POST"
                        action="{{ action('App\Http\Controllers\PostsController@destroy', $post->id) }}"
                    >
                        @csrf
                        @method('DELETE')
                        
                        <button class="btn btn-danger">削除する</button>
                    </form>
                </div>
            @endif
            @endhasrole
            @endauth
        @endif
```


メモ：あれこれアクセス制御を調べているとgateとpoliciyを知ったのでリンクを記載します。
https://qiita.com/nunulk/items/719e1d53c455946184ac
https://reffect.co.jp/laravel/laravel-gate-policy-understand
https://www.ritolab.com/entry/56

<br>

## 3. 気が付いたエラーの修正や気になる点の修正

#### 投稿の新規作成時の名前入力欄の修正

赤枠が不要なのでviewから削除し、ログインしているユーザーのIDがDBに入るように変更します。
名前を入力して投稿するとエラーが出るので、この解決にもつながると思います。

![スクリーンショット (44).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/485388/01aae112-26af-0989-ac69-b0fa8f1be85c.png)

削除箇所をわかりやすいようにコメントアウトしていますが、不必要なので削除してください。

```resources\views\bbs\create.blade.php
                <!-- <div class="form-group">
                    <label for="subject">
                        名前
                    </label>
                    <input
                        id="name"
                        name="name"
                        class="form-control {{ $errors->has('name') ? 'is-invalid' : '' }}"
                        value="{{ old('name') }}"
                        type="text"
                    >
                    @if ($errors->has('name'))
                        <div class="invalid-feedback">
                            {{ $errors->first('name') }}
                        </div>
                    @endif
                </div> -->
```

続いてコントローラの修正をします。

```app\Http\Requests\PostRequest.php
use Illuminate\Support\Facades\Auth; // ←　追記

    /**
     * バリデーション、登録データの整形など
     */
    public function store(PostRequest $request)
    {
        $id = Auth::id();
        $savedata = [
            'user_id' => $id, // ← 変更　'name' => $request->name,
            'subject' => $request->subject,
            'message' => $request->message,
            'category_id' => $request->category_id,
        ];
     
        $post = new Post;
        $post->fill($savedata)->save();
     
        return redirect('/bbs')->with('poststatus', '新規投稿しました');
    }
```

ログインしているユーザーのidを取るためにAuthを使うので、useを追記します。
取ってきたidを\$idに入れて、\$savedataのnameだったところを修正します。
下記のような書き方も出来るので、一応参考のため記載しておきます。

```app\Http\Requests\PostRequest.php
    public function store(PostRequest $request)
    {
        $id = Auth::id();
        $post = new Post;
        $post->user_id = $id;
        $post->fill($request->all())->save();
    }
```

続いて、このままではバリデーションに引っ掛かるのでPostRequestを修正します。

```app\Http\Requests\PostRequest.php
    public function rules()
    {
        return [
            // 'name' => 'required|max:40', // ←　削除
            'subject' => 'required|max:80',
            'message' => 'required|max:350',
            'category_id' => 'required|integer',
        ];
    }

    /**
     * エラーメッセージを日本語化
     * 
     */
    public function messages()
    {
        return [
            // 'name.required' => '名前を入力してください', // ←　削除
            // 'name.max' => '名前は40文字以内で入力してください', // ←　削除
            'subject.required' => '件名を入力してください',
            'subject.max' => '件名は80文字以内で入力してください',
            'message.required' => 'メッセージを入力してください',
            'message.max' => 'メッセージは350文字以内で入力してください',
            'category_id.required' => 'カテゴリーを選択してください',
            'category_id.integer' => 'カテゴリーの入力形式が不正です',
        ];
    }
```

$requestにはnameが入っていないので、メッセージと合わせて削除します。

<br>

#### 投稿の編集の名前入力欄の修正

同様にこの修正も行います。

![スクリーンショット (45).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/485388/e1dd122e-c659-0c01-8295-22d20b471f82.png)

下記内容を削除で終了です。

```resources\views\bbs\edit.blade.php
                <!-- <div class="form-group">
                    <label for="subject">
                        名前
                    </label>
                    <input
                        id="name"
                        name="name"
                        class="form-control {{ $errors->has('name') ? 'is-invalid' : '' }}"
                        value="{{ old('name') ?: $post->user->name }}"
                        type="text"
                    >
                    @if ($errors->has('name'))
                        <div class="invalid-feedback">
                            {{ $errors->first('name') }}
                        </div>
                    @endif
                </div> -->
```

<br>

#### 投稿の詳細ページの名前入力欄の修正

コメント部分の修正を行います。
Comments Controller回りの修正なので、新規作成時と同様にログインしているユーザーのIDがDBに入るように変更します。

![スクリーンショット (46).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/485388/07e0a8c4-3e40-2257-6999-ea07dc85dbb3.png)

```resources\views\bbs\show.blade.php
        @if (Route::has('login'))
            @auth
                <form class="mb-4" method="POST" action="{{ route('comment.store') }}">
                    @csrf
                
                    <input
                        name="post_id"
                        type="hidden"
                        value="{{ $post->id }}"
                    >
                
                    <!-- <div class="form-group">　// ここから
                        <label for="subject">
                        名前
                        </label>
                
                    <input
                            id="name"
                            name="name"
                            class="form-control {{ $errors->has('name') ? 'is-invalid' : '' }}"
                            value="{{ old('name') }}"
                            type="text"
                        >
                        @if ($errors->has('name'))
                        <div class="invalid-feedback">
                        {{ $errors->first('name') }}
                        </div>
                        @endif
                    </div> -->　// ここまで
                
                    <div class="form-group">
                        <label for="body">
                        本文
                        </label>
                    
                        <textarea
                            id="comment"
                            name="comment"
                            class="form-control {{ $errors->has('comment') ? 'is-invalid' : '' }}"
                            rows="4"
                        >{{ old('comment') }}</textarea>
                        @if ($errors->has('comment'))
                        <div class="invalid-feedback">
                        {{ $errors->first('comment') }}
                        </div>
                        @endif
                    </div>
                
                    <div class="mt-4">
                        <button type="submit" class="btn btn-primary">
                        コメントする
                        </button>
                    </div>
                </form>
            @endauth
        @endif
```

削除箇所（ここから　ここまで）をコメントアウトしています。

```app\Http\Controllers\CommentsController.php
use Illuminate\Support\Facades\Auth; // ←　追記

    public function store(CommentRequest $request)
    {
        $id = Auth::id(); // ←　追記
        $savedata = [
            'post_id' => $request->post_id,
            'user_id' => $id, // ← 'name' => $request->name, から変更
            'comment' => $request->comment,
        ];

        $comment = new Comment;
        $comment->fill($savedata)->save();

        return redirect()->route('bbs.show', [$savedata['post_id']])->with('commentstatus','コメントを投稿しました');
    }
```

同様に、ログインしているユーザーのidを取るためにAuthを使うので、useを追記します。
取ってきたidを\$idに入れて、\$savedataのnameだったところを修正します。

続いて、同様にバリデーションに引っ掛かるのでCommentRequestを下記のように修正します。

```app\Http\Requests\CommentRequest.php
    public function rules()
    {
        return [
            // 'name' => 'required|max:40', // ←　削除
            'comment' => 'required|max:350',
        ];
    }

    /**
     * エラーメッセージを日本語化
     * 
     */
    public function messages()
    {
        return [
            // 'name.required' => '名前を入力してください', // ←　削除
            // 'name.max' => '名前は40文字以内で入力してください', // ←　削除
            'comment.required' => 'コメント本文を入力してください',
            'comment.max' => 'コメント本文は350文字以内で入力してください',
        ];
    }
```

以上で、アクセス制御の修正は終了です。

一応、ルーティングで気が付いた箇所があったので下記のように修正します。

```routes\web.php
Route::resource('/bbs', PostsController::class)->parameter('bbs', 'post')->only([
    'index', 'show', 'create', 'store', 'edit', 'update', 'destroy'
]);

↓　変更　↓

Route::resource('/bbs', PostsController::class)->parameter('bbs', 'post');
```