ユーザー管理の実装からの続きです。
ロール管理機能を追加していきます。
ロール管理画面の概要
機能
ロール管理画面も、基本的にはユーザー管理と同じように作成します。必要な機能は以下の通りです。
- 新しいロールの作成
- 新しい権限の追加
- ロール一覧
- ロール名
- slug
- 権限リスト(全ての権限を並べたチェックボックス)
- 修正ボタン
- 削除ボタン
アクセス権限
ユーザーが持つrole.view, role.create, role.update, role.delete の権限でチェックします。
ロール管理用のコントローラーの作成
ユーザー管理の時と同様に、resourceフラグでコントローラーを作成して、ルートに適用します。
ロール用のコントローラーを作成
- ターミナルから、以下を実行して、ロール用のリソースコントローラーを作成
php artisan make:controller Sentinel/RoleController --resource
パーミッションは、追加と削除のみなので、手動でコントローラーとルートを作成します。
- app/Http/Controllers/Sentinel/PermissionController.php を新規に作成してエディターで開く
- 以下のコードを追加
<?php
namespace App\Http\Controllers\Sentinel;
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Http\Controllers\Controller;
class PermissionController extends Controller
{
@param
@return
public function store(Request $request)
{
}
@param
@return
public function destroy($id)
{
}
}
ルートを設定
作成したコントローラーへのアクセスをルートに追加します。
- app/Http/routes.php をエディターで開く
- 以下の行をusersの定義の下に追加して、ロールのためのルートを追加
Route::resource('roles', 'Sentinel\RoleController');
Route::post('permissions', 'Sentinel\PermissionController@store');
Route::delete('permissions/{name}', 'Sentinel\PermissionController@destroy');
ロール管理画面の開発
ビューを表示するためのコードを作成
ロール管理画面用のビューを呼び出すルートを追加します。
- app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
- あらかじめ作成されているindex()メソッドを、以下のようにコード追加する
public function index()
{
return view('sentinel.roles', ['permissions' => self::getPermissionList()]);
}
パーミッション一覧を取得するためのコードを追加
パーミッション一覧を表示するために、現在登録されている全てのロールが持っている、全てのパーミッション名のリストが必要なので、それを取得するための関数を作成します。
- 引き続き、app/Http/Controllers/Sentinel/RoleController.php で作業する
- Sentinelを参照するために、コードの上の方に以下を追加する
use Sentinel;
- RoleControllerクラスに以下の関数を追加する
/**
* 全ロールに設定されているパーミッションのリストを作成
*/
public static function getPermissionList() {
$permissions = [];
foreach(Sentinel::getRoleRepository()->all() as $role) {
foreach($role->permissions as $k => $v) {
if (!in_array($k, $permissions)) {
$permissions[] = $k;
}
}
}
return $permissions;
}
ビューの作成
ビューを作成します。パーミッションは単純な追加と削除のみなので、今回はロールとパーミッションの両方をまとめて管理するページにします。
エラー表示ブロックの共有化
エラーが発生した要素ごとにエラーメッセージを表示させようと思います。各要素で必要なコードは共通ですので、共通のビューを作成して、includeで読み込ませて使えるようにします。
- resources/views/parts/error-block.blade.php ファイルを作成して、エディターで開く
- 以下のコードを入力
@if ($errors->has($name))
<span class="help-block">
<strong>{{ $errors->first($name) }}</strong>
</span>
@endif
これを使うには、入力要素を以下のような形にして、inputタグに続けて@includeでerror-blockを読み込み、引数としてinputタグのnameを指定します。
*** 使用例
<div class="form-group{{ $errors->has('入力要素のname') ? ' has-error' : '' }}">
<input name="入力要素のname" class="form-control" ・・・
@include('parts.error-block', ['name' => '入力要素のname'])
</div>
モーダルウィンドウの作成
ロールの変更や削除の選択をする時のBootstrapのモーダルウィンドウ用のビューを作成します。
- resources/views/parts/modal.blade.php を作成して、以下のコードを入力する
{{-- 以下のように呼び出しボタンを用意する
<button type="button" class="btn" data-toggle="modal" data-target="#ターゲット">
ボタン名
</button>
id 呼び出すID
title モーダルに表示するタイトル
body モーダルの本文
action 実行時のURL
method 実行時のメソッド
--}}
<div class="modal fade" id="{{$id}}" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">{{$title}}</h4>
</div>
<div class="modal-body">
{{$body}}
</div>
<div class="modal-footer">
<form action="{{$action}}" method="POST">
{{ csrf_field() }}
{{ method_field($method) }}
<button type="submit" class="btn btn-primary">
<i class="fa fa-btn fa-check"></i> はい
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fa fa-btn fa-close"></i>いいえ</button>
</form>
</div>
</div>
</div>
</div>
ボタンごとにFormがないものも用意します。
- resources/views/parts/modal-no-form.blade.php を作成して、以下のコードを入力する
{{-- モーダルのform未組み込み版
以下のように呼び出しボタンを用意する
<button type="button" class="btn" data-toggle="modal" data-target="#ターゲット">
ボタン名
</button>
id 呼び出すID
title モーダルに表示するタイトル
body モーダルの本文
--}}
<div class="modal fade" id="{{$id}}" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">{{$title}}</h4>
</div>
<div class="modal-body">
{{$body}}
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">
<i class="fa fa-btn fa-check"></i> はい
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fa fa-btn fa-close"></i>いいえ
</button>
</div>
</div>
</div>
</div>
ロール管理ビューの作成
- resources/views/sentinel/roles.blade.php ファイルを作成して、エディターで開く
- 以下のコードを入力
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-12">
@include('parts.info')
@include('common.errors')
<h4>パーミッション</h4>
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>パーミッション名</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<form class="col" role="form" method="POST" action="{{url('permissions')}}">
<tr>
{{ csrf_field() }}
<td>
<div class="form-group{{ $errors->has('new_permission') ? ' has-error' : '' }}">
<input id="new_permission" type="text" class="form-control" name="new_permission" value="{{ old('new_permission') }}">
@include('parts.error-block', ['name' => 'new_permission'])
</div>
</td>
<td>
<button type="submit" class="btn btn-primary">
<i class="fa fa-btn fa-plus"></i> 新規登録
</button>
</td>
</tr>
</form>
</tbody>
</table>
<h4>パーミッションの削除</h4>
<div class="row">
<div class="col-md-12">
@foreach($permissions as $permission)
<button type="button" class="btn btn-default" aria-label="Close" data-toggle="modal"
data-target="#delete-permission-{{array_search($permission, $permissions)}}">
<i class="fa fa-btn fa-remove"></i> {{$permission}}
</button>
@include('parts.modal', [
'id' => 'delete-permission-'.array_search($permission, $permissions),
'title' => trans('sentinel.delete_permission_title'),
'body' => trans('sentinel.confirm_delete_permission').' : '.$permission,
'action' => url('permissions', base64_encode($permission)),
'method' => 'DELETE'
])
@endforeach
</div>
</div>
<hr>
<h4>ロール追加</h4>
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>ロール名(日本語可)</th>
<th>Slug</th>
<th>パーミッション</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<form class="col" role="form" method="POST" action="{{url('roles')}}">
{{ csrf_field() }}
<tr>
<td>
<div class="form-group{{ $errors->has('new_role') ? ' has-error' : '' }}">
<input id="new_role" type="text" class="form-control" name="new_role" value="{{ old('new_role') }}">
@include('parts.error-block', ['name' => 'new_role'])
</div>
</td>
<td>
<div class="form-group{{ $errors->has('new_slug') ? ' has-error' : '' }}">
<input id="new_slug" type="text" class="form-control" name="new_slug" value="{{ old('new_slug') }}">
@include('parts.error-block', ['name' => 'new_slug'])
</div>
</td>
<td>
@foreach ($permissions as $per)
<div>
<input type="checkbox" name="new_per_{{str_replace(".", "-", $per)}}"
{{old("new_per_".str_replace(".", "-", $per))=="on" ? 'checked="true"' : ''}}
> {{$per}}
</div>
@endforeach
</td>
<td>
<button type="submit" class="btn btn-primary">
<i class="fa fa-btn fa-plus"></i> 新規登録
</button>
</td>
</tr>
</div>
</form>
</tbody>
</table>
<h4>ロール一覧</h4>
<table class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th>ロール名(日本語可)</th>
<th>Slug</th>
<th>パーミッション</th>
<th colspan="2">操作</th>
</tr>
</thead>
<tbody>
@foreach(Sentinel::getRoleRepository()->all() as $role)
<tr>
<form class="col" role="form" method="POST" action="{{url('roles', $role->id)}}">
{{ csrf_field() }}
{{ method_field('PUT') }}
<td>
<div class="form-group{{ $errors->has('role_'.$role->id.'_name') ? ' has-error' : '' }}">
<input type="text"
class="form-control"
name="role_{{$role->id}}_name"
id="role_{{$role->id}}_name"
value="{{empty(old('role_'.$role->id.'_name')) ? $role->name : old('role_'.$role->id.'_name')}}">
@include('parts.error-block', ['name' => 'role_'.$role->id.'_name'])
</div>
</td>
<td>
<div class="form-group{{ $errors->has('role_'.$role->id.'_slug') ? ' has-error' : '' }}">
<input type="text"
class="form_control"
name="role_{{$role->id}}_slug"
id="role_{{$role->id}}_slug"
value="{{empty(old('role_'.$role->id.'_slug')) ? $role->slug : old('role_'.$role->id.'_slug')}}">
@include('parts.error-block', ['name' => 'role_'.$role->id.'_slug'])
</div>
</td>
<td>
@foreach ($permissions as $per)
<div>
<input type="checkbox"
name="role_{{$role->id}}_per_{{str_replace(".", "-", $per)}}"
@if (old("role_".$role->id."_per_".str_replace(".", "-", $per))=="on")
checked="true"
@elseif ($role->hasAccess($per))
checked="true"
@endif
> {{$per}}
</div>
@endforeach
</td>
<td>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#role_{{$role->id}}_update">
<i class="fa fa-btn fa-refresh"></i> 変更
</button>
@include('parts.modal-no-form', [
'id' => 'role_'.$role->id.'_update',
'title' => 'ロールの更新',
'body' => 'ロール['.$role->name.']の情報を更新しますか?',
])
</td>
</form>
<td>
<button type="submit" class="btn btn-danger" data-toggle="modal" data-target="#role-{{$role->id}}-delete">
<i class="fa fa-btn fa-remove"></i> 削除
</button>
@include('parts.modal', [
'id' => 'role-'.$role->id.'-delete',
'title' => 'ロールの削除',
'body' => 'ロール['.$role->name.']を削除しますか?',
'action' => url('roles', $role->id),
'method' => 'DELETE',
])
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection
以上で、ロール管理画面のフォームの表示まで出来ました。 http://0.0.0.0:8080/roles にアクセスしてみてください。画面が表示されて、利用されているパーミッションリストやロール一覧が確認できます。
まだ、ボタンの処理は実装していないので、何かを押してもエラーが発生します。それぞれの機能を追加していきます。
ロールに設定できるパーミッションの種類を追加する処理を追加するために、 PermissionController.php のstore関数にコードを追加します。
- app/Http/Controllers/Sentinel/PermissionController.php をエディターで開く
- ロールコントローラーやSentinel、リダイレクトを利用するために以下の3つのuseを最初の方に追加
use App\Http\Controllers\Sentinel\RoleController;
use Sentinel;
use Redirect;
- 先に作成したstore関数を以下のように中身を実装する
public function store(Request $request)
{
// バリデーション
$this->validate($request, [
// nameは必須で、255文字まで
'new_permission' => 'required|max:255',
]);
// 既存なら何もしない
$nowper = RoleController::getPermissionList();
if (in_array($request->new_permission, $nowper)) {
// すでにあるので、エラーで返す
return Redirect::back()->withInput()->withErrors(['new_permission' => trans('sentinel.same_permission')]);
}
// 作成実行
foreach(Sentinel::getRoleRepository()->all() as $role) {
$role->addPermission($request->new_permission, false)->save();
}
// 成功
return Redirect::back()->with(['info' => trans('sentinel.permission_add_done').":".$request->new_permission]);
}
以上で、パーミッションの追加ができるようになりました。ロール管理画面を表示して、「test」などの名前のパーミッションの登録をしてみてください。
パーミッションを削除する機能を追加します。引き続き、PermissionController.php で作業をします。
- あらかじめ作成していた PermissionController.php の destroy関数を、以下のように実装する
public function destroy($name)
{
$per = base64_decode($name);
$permissions = RoleController::getPermissionList();
if (!in_array($per, $permissions)) {
return Redirect::back()->withErrors(['delete_permission' => trans('sentinel.invalid_permission')]);
}
// 削除
foreach (Sentinel::getRoleRepository()->all() as $role) {
$role->removePermission($per)->save();
}
return Redirect::back()->with(['info' => trans('sentinel.permission_delete_done').":".$per]);
}
以上で削除が動作するようになります。先ほど追加した「test」などのパーミッションを、削除欄から選択して消してみてください。
ロールの追加
ロールを追加する処理をRoleController.phpに追加します。
- app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
- Redirectを使えるように、ファイルの最初の方に以下のuse文を追加
use Redirect;
- あらかじめ作成されている store 関数を以下のように実装する
public function store(Request $request)
{
$this->validate($request, [
'new_role' => 'required|max:255',
'new_slug' => 'required|max:255',
]);
// パーミッションリストの作成
$permissions = [];
$pers = self::getPermissionList();
foreach($pers as $per) {
$permissions[$per] = $request['new_per_'.str_replace(".", "-", $per)] == "on";
}
$role = Sentinel::getRoleRepository()->createModel()->create([
'name' => $request->new_role,
'slug' => $request->new_slug,
'permissions' => $permissions
]);
return Redirect::back()->with(['info' => trans('sentinel.role_create_done')]);
}
以上でロールが追加できるようになります。「testroll」などの名前で登録してみてください。
ロールの変更
ロールの変更処理を実装します。
- app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
- あらかじめ作成されている update 関数を以下のように実装する
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
$idx = 'role_'.$id.'_';
// バリデーションを実施
$this->validate($request, [
$idx.'name' => 'required|max:255',
$idx.'slug' => 'required|max:255',
]);
// 修正をチェック
$updates = [];
$updates[] = trans('sentinel.role_update_done');
// 現ロールを取得
$role = Sentinel::findRoleById($id);
if ($role == null) {
return Redirect::back()->withInput()->withErrors(['invalid_role' => trans('sentinel.invalid_role')]);
}
// 名前チェック
if ($role->name !== $request[$idx."name"]) {
$updates[] = "ロール名: ".$role->name." > ".$request[$idx."name"];
$role->name = $request[$idx."name"];
}
// slugチェック
if ($role->slug !== $request[$idx."slug"]) {
$updates[] = "Slug: ".$role->slug." > ".$request[$idx."slug"];
$role->slug = $request[$idx."slug"];
}
// パーミッションの設定
$permissions = self::getPermissionList();
foreach($permissions as $per) {
if ($role->hasAccess($per) && (!$request[$idx."per_".str_replace(".", "-", $per)])) {
$updates[] = $per." > off";
$role->updatePermission($per, false);
}
else if (!$role->hasAccess($per) && ($request[$idx."per_".str_replace(".","-",$per)])) {
$updates[] = $per." > on";
$role->updatePermission($per, true);
}
}
// 更新
if (count($updates) > 1) {
$role->save();
return Redirect::back()->with(['info' => $updates]);
}
// 更新なし
return Redirect::back()->with(['info' => trans('sentinel.no_changed')]);
}
info.blade.phpでエラーが発生したら、以下のように修正する
@if (session('info') || isset($info))
<div class="alert alert-info">
@if (session('info'))
@if (is_array(session('info')))
<ul>
@foreach(session('info') as $ln)
<li>{{$ln}}</li>
@endforeach
</ul>
@else
{{ session('info') }}
@endif
@endif
@if (isset($info))
{{ $info }}
@endif
</div>
@endif
以上で完了です。登録した「test」ロールの名前やslug、パーミッションを変更して、「変更」を押して、正しく情報が更新されることを確認してください。
ロールの削除
最後に、ロールを削除する処理を実装します。
- app/Http/Controller/Sentinel/RoleController.php をエディターで開く
- あらかじめ作成されている destroy 関数を以下のように実装する
public function destroy($id)
{
// 削除実行
$role = Sentinel::findRoleById($id);
if ($role === null) {
return Redirect::back()->withInput()->withErrors(['role-'.$id.'_delete', trans('sentinel.invalid_role')]);
}
$name = $role->name;
// 削除実行
$role->delete();
return Redirect::back()->with(['info' => trans('sentinel.role_delete_done')." : ".$name]);
}
以上で完了です。「test」ロールを削除してみてください。
機能が実装できたので、各操作がパーミッションを持つユーザーのみ操作できるように、コントローラーのコンストラクターにコードを追加します。
- app/Http/Controllers/Sentinel/RoleController.php をエディターで開く
- 以下のコンストラクターをクラスに追加する
/**
* コンストラクター
* 処理に権限チェックのミドルウェアを設定
*/
public function __construct() {
$this->middleware('permission:role.view', [
'only' => [
'index'
]
]);
$this->middleware('permission:role.update', [
'only' => [
'update'
]
]);
$this->middleware('permission:role.create', [
'only' => [
'store'
]
]);
$this->middleware('permission:role.delete', [
'only' => [
'destroy'
]
]);
}
- app/Http/Controllers/Sentinel/PermissionController.php をエディターで開く
- 以下のコンストラクターをクラスに追加する
/**
* コンストラクター
* 処理に権限チェックのミドルウェアを設定。パーミッションの権限はロールに準じる
*/
public function __construct() {
$this->middleware('permission:role.create', [
'only' => [
'store'
]
]);
$this->middleware('permission:role.delete', [
'only' => [
'destroy'
]
]);
}
これで、ロールやパーミッションの各操作は、以下のパーミッションが有効なロールを持ったユーザーのみが実行できるようになります。
- ロールの操作
- ロールリストの閲覧 role.view
- ロールの追加 role.create
- ロールの編集 role.update
- ロールの削除 role.delete
- パーミッションの操作
できたら、権限のないユーザーで http://0.0.0.0:8080/roles にアクセスしてみましょう。権限がないので、しばらく元の画面に戻ろうとしたのちにエラーが表示されます。通常はrolesに直接アクセスすることはないので、遷移元のページに戻ります。
最後に
最適な例にはなっていないと思いますが、LaravelとSentinelのサンプルとして公開しました。必要な作業が思ったより多かったですが、それでもこれらのフレームワークを使わないとさらに膨大な手間がかかりますので、とてもありがたい環境です。PHPもまだまだいけてます。