Laravelで作られた掲示板に機能を追加する②の続きです。
目標
- アクセス制限を実装
- Laravel-permissionのインストール ~ seedの流し込み
- viewの調整
- 気が付いたエラーの修正
色々と出来そうで触ってみたいという理由から、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
Laravel-permissionのインストール ~ seedの流し込み
Laravel-permissionのインストール
$ composer require spatie/laravel-permission
二か所に追記
protected $routeMiddleware = [
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class, // ← 追加
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class, // ← 追加
'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class, // ← 追加
]
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
DBデータの作成
各シーダーのひな形作成を作り、データを流し込みます。
1. PermissionTableSeeder.php
シーダーのひな形作成
$ php artisan make:seeder PermissionTableSeeder
admminとuserを作成
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を確認してみてください。
2. RoleTableSeeder.php
手順は同じなので説明を省きます。
$ php artisan make:seeder RoleTableSeeder
<?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
3. RoleHasPermissionTableSeeder.php
$ php artisan make:seeder RoleHasPermissionTableSeeder
<?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
4. UserTableSeeder.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');
}
もしダミーデータのユーザー数が異なる場合は、下記の箇所を修正してください。
理由は、ダミーデータのユーザー数と合わせていないと、整合性が取れずにエラーが出るからです。
public function definition()
{
return [
'user_id' => $this->faker->numberBetween(1,5), // ← 5となっている箇所を、ダミーデータのユーザー数に変更
];
}
seedの読み込みでエラーが出た場合は、気にせずにDatabaseSeeder.phpに進んでください。
$ php artisan db:seed --class=UserTableSeeder
5. DatabaseSeeder.php
php artisan db:seed --class=なんちゃらでデータを流し込んでいたので、refreshやfreshのことを考え、DataBaseSeeder.phpにも書き込んでおきます。
追記は必ずUsersTableSeederの上にしてください。下に書いた場合は、DBの整合性が取れずにエラーになります。
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 の違い
とりあえずこれでLaravel permissionのインストールとダミーデータの流し込みが終わりました。
[メモ]
自分の知識では、アクセス制御というのは、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
2. viewの編集
いい方法が思いつかなかったので、if文でhasrole('admin')とhasrole('user')の表示を分けました。
さらにuserの時は、@if( ( $post->user_id ) === ( Auth::user()->id ) )
でidが一致していると編集と削除ボタンを表示します。
今後の課題として、もし必要になるならば、管理用のページとユーザー用のページを分けるとかになるかもしれません。
@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
編集・編集ボタンのところを分岐させます
<!-- 編集・編集ボタン -->
@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
3. 気が付いたエラーの修正や気になる点の修正
投稿の新規作成時の名前入力欄の修正
赤枠が不要なのでviewから削除し、ログインしているユーザーのIDがDBに入るように変更します。
名前を入力して投稿するとエラーが出るので、この解決にもつながると思います。
削除箇所をわかりやすいようにコメントアウトしていますが、不必要なので削除してください。
<!-- <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> -->
続いてコントローラの修正をします。
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だったところを修正します。
下記のような書き方も出来るので、一応参考のため記載しておきます。
public function store(PostRequest $request)
{
$id = Auth::id();
$post = new Post;
$post->user_id = $id;
$post->fill($request->all())->save();
}
続いて、このままではバリデーションに引っ掛かるのでPostRequestを修正します。
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が入っていないので、メッセージと合わせて削除します。
投稿の編集の名前入力欄の修正
同様にこの修正も行います。
下記内容を削除で終了です。
<!-- <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> -->
投稿の詳細ページの名前入力欄の修正
コメント部分の修正を行います。
Comments Controller回りの修正なので、新規作成時と同様にログインしているユーザーのIDがDBに入るように変更します。
@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
削除箇所(ここから ここまで)をコメントアウトしています。
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を下記のように修正します。
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文字以内で入力してください',
];
}
以上で、アクセス制御の修正は終了です。
一応、ルーティングで気が付いた箇所があったので下記のように修正します。
Route::resource('/bbs', PostsController::class)->parameter('bbs', 'post')->only([
'index', 'show', 'create', 'store', 'edit', 'update', 'destroy'
]);
↓ 変更 ↓
Route::resource('/bbs', PostsController::class)->parameter('bbs', 'post');