読者です 読者をやめる 読者になる 読者になる

tanaka's Programming Memo

プログラミングについてのメモ。

Laravel+Sentinel ロール管理機能を作成する

PHP Laravel Sentinel

ユーザー管理の実装からの続きです。

ロール管理機能を追加していきます。

ロール管理画面の概要

機能

ロール管理画面も、基本的にはユーザー管理と同じように作成します。必要な機能は以下の通りです。

  • 新しいロールの作成
    • ロール名
    • slug
    • 追加ボタン
  • 新しい権限の追加
    • 権限の文字列のみ
    • 追加ボタン
  • ロール一覧
    • ロール名
    • 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
{
    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    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">&times;</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><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

ボタンごとに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">&times;</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><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->
ロール管理ビューの作成
  • 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'
            ]
        ]);
    }

これで、ロールやパーミッションの各操作は、以下のパーミッションが有効なロールを持ったユーザーのみが実行できるようになります。

できたら、権限のないユーザーで http://0.0.0.0:8080/roles にアクセスしてみましょう。権限がないので、しばらく元の画面に戻ろうとしたのちにエラーが表示されます。通常はrolesに直接アクセスすることはないので、遷移元のページに戻ります。

最後に

最適な例にはなっていないと思いますが、LaravelとSentinelのサンプルとして公開しました。必要な作業が思ったより多かったですが、それでもこれらのフレームワークを使わないとさらに膨大な手間がかかりますので、とてもありがたい環境です。PHPもまだまだいけてます。

Laravel+Sentinelで、ユーザー管理を実装する

PHP Laravel Sentinel

Laravelの認証や認可にSentinelを組み込んだのであれば、ユーザーの役割(ロール)や権限(パーミッション)機能を使えるようにしたいところです。

そこで、管理者によるユーザー管理、ロールとパーミッションによる権限の管理をするコントローラーやビューを作成します。

前提

  • こちらの記事に続けて実装
  • 管理をシンプルにするために、パーミッションはロールにのみ割り当てて、ユーザーに個別に割り当てる機能は設けない
  • ユーザー関連の機能は以下の通り
    • ユーザー一覧の表示(Administrator/Moderator)
    • ユーザーの新規作成(Administrator/Moderator)
    • ユーザーの編集(Administrator)
      • ロールの切り替え
    • ユーザーの削除(Administrator)
  • ロール関連の機能は以下の通り
    • ロールの新規作成
    • ロールの編集
      • 権限の追加、編集、削除
    • ロールの削除
  • 権限チェック用のミドルウェアを用意する
    • 指定のロールの時にパス
    • 指定の権限の時にパス
  • 管理者は、設定で指定のメールアドレスでも指定できるようにする

管理者のメールアドレスを設定

ユーザー登録をしただけでは、ロールが設定されないので管理ができません。そこで、.envに管理者メールアドレスを定義して、ユーザーがそのメールアドレスだった場合は管理者として扱うようにします。

  • config/app.php をエディターで開く
  • 以下を追加
    // 管理者用EMailアドレスがあれば設定
    'admin_email' => env('ADMIN_EMAIL', ''),
  • .env をエディターで開く
  • 以下を追加。メールアドレスは適宜変更のこと
ADMIN_EMAIL=your@emai.com

以上で、 config('app.admin_email') とすると、管理者用のメールアドレスが確認できます。これを管理者チェック用のミドルウェアに組み込みます。

このメールアドレスのユーザーは、すべての操作を可能にします。

ログイン時にロールを割り当て

ログインしたのが管理者の時、ロールが作られているのかのチェックと、ロールが管理者に割り当てられているかのチェックを行います。いずれもまだの場合は設定を行います。ロールや権限の初期設定のためのデフォルトデータをコンフィグファイルにまとめておきましょう。

ロール関連の設定を作成

  • config/roles.php を新規作成して、エディターで開く
  • とりあえず、AdministratorとModeratorを定義しておく。権限は必要に合わせて変更の事
<?php
/**
 * ロール用の設定ファイル
 * @copyright 2016 YuTanaka@AmuseOne
 */

return [
    "default_roles" => [
        "default_admin_roles" => [
            "name" => "Administrator",
            "slug" => "admin",
            "permissions" => [
                "user.create" => true,
                "user.delete" => true,
                "user.view" => true,
                "user.update" => true,
                "role.create" => true,
                "role.delete" => true,
                "role.view" => true,
                "role.update" => true,
            ]
        ],
        "default_moderator" => [
            "name" => "Moderator",
            "slug" => "moderator",
            "permissions" => [
                "user.create" => true,
                "user.delete" => false,
                "user.view" => true,
                "user.update" => false,
                "role.create" => false,
                "role.delete" => false,
                "role.view" => false,
                "role.update" => false,
            ]
        ]
    ]
];

管理者メールの時の処理

SentinelControllerのログイン時の処理に、今回の処理を組み込みます。作業の前に、ロールや権限の確認方法を調べておきます。

ログインユーザーのロールの確認

現在ログインしているユーザーが特定のロールかを確認するには、 Sentinel::inRole('ロールのslug') でチェックします。ログインしていない場合は、 ErrorException が発生するので、try-catchで対応するか、事前にログインチェックをします。

if (Sentinel::inRole('slug')) {
    // ユーザーはslugのロールを持ちます
}
ログインユーザーのパーミッションの確認

パーミッションをチェックするには、ユーザーのインターフェースを取得して、必要に応じて次の2つの方法を利用します。複数のパーミッションを全て満たすかを判定するには、 $user->hasAccess() でチェックします。どれか一つでも有効であれば良い場合は、 $user->hasAnyAccess() でチェックします。

$user = Sentinel::check();
if ($user->hasAccess(['user.create', 'user.delete'])) {
    // ユーザーの作成と削除、どちらもできる場合の処理
}

if ($user->hasAnyAccess(['user.create', 'user.delete'])) {
    // ユーザーの作成、または、削除ができる場合の処理
}

以上を活用して、ロールをチェックして、必要な処理を加えます。

ロールのセットアップと管理者の割り当て処理

  • app/Http/Controllers/Sentinel/SentinelController.php をエディターで開く
  • クラスに以下の関数を追加
    /**
     * 管理者のメールアドレスチェックをして、管理者の時で、ロールがない時は、
     * ロールを整える
     * @return bool true=作成した / false=何もしない
     */
    private function checkAdminMailRoles(Request $request) {
        if (strcmp($request['email'], config('app.admin_email')) !== 0) {
            return false;
        }

        // ロールがあるかを確認する
        if (!Sentinel::findRoleBySlug('admin')) {
            // ロールがないので、作成する
            $defs = config('roles.default_roles');
            foreach($defs as $k => $v)
            {
                // ロールがなければ作成
                $role = Sentinel::findRoleByName($defs[$k]['name']);
                if (!$role) {
                    Sentinel::getRoleRepository()->createModel()->create($defs[$k]);
                }
                else {
                    // ロールがある場合は、更新
                    $role->permissions = $defs[$k]['permissions'];
                    $role->save();
                }
            }
        }

        // 管理者のメール。管理者ロールが割り当てられている場合は何もしない
        if (Sentinel::inRole('admin')) {
            return false;
        }

        // ユーザーにadminロールを設定する
        $role = Sentinel::findRoleBySlug('admin');
        $role->users()->attach($this->userInterface);
        return true;
    }
  • 次に、これをログイン処理内で呼び出す。login()メソッドの最後である return redirect($this->redirectTo); の前の行に、以下の関数呼び出しを追加する
        // ロールのチェック
        $this->checkAdminMailRoles($request);

以上で、管理者メールに設定したユーザーでログインすると、初期のロールと、 ユーザーに管理者ロール(Administrator)が割り当てられます。

メニューの追加

管理者としてログインしている時は、「ユーザー管理」と「ロール管理」。モデレーターとしてログインしている時は、「ユーザー」メニューを表示するようにします。

  • resources/views/layouts/app.blade.php をエディターで開く
  • Homeメニューが描かれている Left Side Of Navbar 欄を探して、以下のように修正
                <!-- Left Side Of Navbar -->
                <ul class="nav navbar-nav">
                    <li><a href="{{ url('/home') }}">Home</a></li>
                    @if (Sentinel::check())
                        @if (Sentinel::inRole('admin'))
                            <li><a href="{{ url('users')}}">ユーザー管理</a></li>
                            <li><a href="{{ url('roles')}}">ロール管理</a></li>
                        @elseif (Sentinel::inRole('moderator'))
                            <li><a href="{{ url('users')}}">ユーザー管理</a></li>
                        @endif
                    @endif
                </ul>

                <!-- Right Side Of Navbar -->

以上で動作確認すると、管理者でログインすると画面上部にユーザー管理とロール管理メニューが表示され、ログインしていなかったり、管理者じゃないユーザーがログインしても、メニューが表示されないことが確認できます。

ユーザー管理を作成

ユーザーについて、いわゆるCRUD(作成=Create、表示=Read、情報変更=Update、削除=Delete)操作を一通り実装します。また、ロールの変更機能もつけます。

作成をページ最上部、その下に編集可能な一覧を表示して、情報更新、削除ボタンを配置するようなビューを作成します。

情報の登録、あるいは更新、削除、それぞれ、確認画面も用意します。

編集不可の場合は、disabledを設定します。

コントローラーの作成

CRUD操作を実装するためのコントローラーを作成する場合は、Laravel artisanのresourceフラグを利用すると便利です。(HTTP Controllers - Laravel - The PHP Framework For Web Artisans)

ユーザーコントローラーを作成して、ルートを追加します。

  • ターミナルでプロジェクトフォルダーから以下のコマンドを実行
php artisan make:controller Sentinel/UserController --resource
  • app/Http/routes.php をエディターで開く
  • 適当な場所に以下の行を追加して、usersに関連するルートを設定
Route::resource('users', 'Sentinel/UserController');

以上で、以下のような一般的なルートが自動的に定義されます。各ルートにアクセスすれば、コントローラーが呼ばれるようになりました。

メソッド パス 呼び出すメソッド名 ルート名 内容 備考
GET /users index users.index ユーザー一覧を表示
GET /users/create create users.create ユーザーの新規登録画面を表示 未使用
POST /users store users.store ユーザーの登録処理
GET /users/{users} show users.show 指定のユーザーの情報を表示 確認画面として利用
GET /users/{users}/edit edit users.edit 指定のユーザーの編集画面を表示 ビューは一覧のものを返す
PUT/PATCH /users/{users} update users.update 指定のユーザーの情報を更新
DELETE /users/{users} destroy users.destroy 指定のユーザーを削除

上記のうち、 users.createは一覧とくっつけるので使用しない。

カスタマイズも可能で、方法は公式ドキュメントにあります。

Usersビューの作成

方針など

ビューを作成しますが、HTMLのフォームはPUTやPATCH、DELETEメソッドが送信できません。そこで、隠し要素にメソッドの種類を渡すことで解決します。以下はPUTを行う例です。

<input type="hidden" name="_method" value="PUT">

ユーザーの登録、編集、ユーザー一覧をまとめたビューを作成します。渡される引数によって、初期値や表示を変更しますが、基本的には1つのビューで対応します。ログインしていない時は事前に弾くので対応不要です。

条件判断は、ロールは $role にslug名を入れて、処理は $routename にルート名を入れて引数に渡すことで判断することにします。

入力欄をパーツ化

ユーザーの登録情報は既にありますので、その中身を使い回せるようにしましょう。

  • resources/views/parts/entry-user.blade.php を作成して、エディターで開く
  • resources/views/auth/register.blade.php をエディターで開く
  • 以下の部分を register.blade.php から切り取って、 entry-user.blade.php に貼り付ける
<div class="form-group{{ $errors->has('name') ? ' has-error' : '' }}">
    <label for="name" class="col-md-4 control-label">Name</label>

    <div class="col-md-6">
        <input id="name" type="text" class="form-control" name="name" value="{{ old('name') }}">

        @if ($errors->has('name'))
            <span class="help-block">
                <strong>{{ $errors->first('name') }}</strong>
            </span>
        @endif
    </div>
</div>

<div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
    <label for="email" class="col-md-4 control-label">E-Mail Address</label>

    <div class="col-md-6">
        <input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}">

        @if ($errors->has('email'))
            <span class="help-block">
                <strong>{{ $errors->first('email') }}</strong>
            </span>
        @endif
    </div>
</div>

<div class="form-group{{ $errors->has('password') ? ' has-error' : '' }}">
    <label for="password" class="col-md-4 control-label">Password</label>

    <div class="col-md-6">
        <input id="password" type="password" class="form-control" name="password">

        @if ($errors->has('password'))
            <span class="help-block">
                <strong>{{ $errors->first('password') }}</strong>
            </span>
        @endif
    </div>
</div>

<div class="form-group{{ $errors->has('password_confirmation') ? ' has-error' : '' }}">
    <label for="password-confirm" class="col-md-4 control-label">Confirm Password</label>

    <div class="col-md-6">
        <input id="password-confirm" type="password" class="form-control" name="password_confirmation">

        @if ($errors->has('password_confirmation'))
            <span class="help-block">
                <strong>{{ $errors->first('password_confirmation') }}</strong>
            </span>
        @endif
    </div>
</div>
  • register.blade.php の切り取った場所に、以下を入力して、パーツを読み込ませる
                        @include('parts.entry-user')

以上ができたら、ユーザー登録画面( http://0.0.0.0:8080/register )を表示して、正常に以前と同じものが表示されればOKです。

ユーザー新規登録のパネルを作成

ユーザー管理画面を作成して、新規登録のための場所に、上記のパーツを読み込ませましょう。

  • resources/views/sentinel/users.blade.php を作成して、エディターで開く
  • 以下のコードを作成
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">

            @if ($role=="admin")

            <form class="col" role="form" method="POST" action="{{url('users')}}">
                {{ csrf_field() }}

                <div class="panel panel-default">
                    <div class="panel-heading">ユーザー新規登録</div>
                    <div class="panel-body">

                        @include('parts.entry-user')

                    </div>
                </div>
            </form>
            @endif

            <table class="table table-striped table-bordered table-hover">
                <thead>
                    <tr>
                        <th>
                            ユーザー名
                        </th>
                        <th>
                            メールアドレス
                        </th>
                        <th>
                            ロール
                        </th>
                        <th colspan='2'>
                            操作
                        </th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>

        </div>
    </div>
</div>
<input type="hidden" name="_method" value="">
@endsection

ユーザーの新規登録と、一覧表示するブロックを定義して、新規登録の中に先ほどパーツ化した parts.entry-user ビューの呼び出しを加えました。また、新規登録は管理者のみの機能なので、 $role が'admin'の時のみ表示するようにしてあります。

テスト用のルート

簡単にテストできるように、テスト用のルートを作成します。ビューが完成したら削除します。

  • app/Http/routes.php をエディターで開く
  • 以下のルートを追加
// TODO: 動作確認が終わったら削除する
Route::get('test/{role}/{routename}', function($role, $routename){
    return view('sentinel.users',
    [
        'role' => $role,
        'method' => $routename,
        'roles' => Sentinel::getRoleRepository()->all(),
        'users' => Sentinel::getUserRepository()->all()
    ]);
});

http://0.0.0.0:8080/test/admin/index にアクセスすると、管理者がユーザー画面を開いた想定で、ビューを呼び出します。また、$rolesにロールの一覧、usersにユーザーの一覧を渡します。

以下をそれぞれ呼び出して、表示内容の変化を確認してください。まだ、渡されたロールやユーザーを表示する機能は組み込んでいませんので、見出しが表示されれば成功です。

ロールの選択ビュー

ユーザーごとに、ロールを設定するためのチェックボックスを表示するBladeのビューを作ります。この機能は新規登録でも、一覧でも、どちらでも利用できるので、使い回せるように作ります。

呼び出すときは、$rolesと、対象のユーザーデータ、ユーザーが保持しているロールを $user に渡します。新規登録の場合はまだユーザーはないので false を渡します。

渡されたユーザーが有効な場合、各ロールを保持しているかどうかをチェックします。ユーザーが該当するロールを持っているかは $user->inRoll('slug名')でチェックできます。

  • resouces/views/parts/role-select.blade.php を作成して、エディターで開く
  • 以下のコードを追加する
<div class="checkbox">
    @foreach($roles as $rl)
    <label class="checkbox-inline" for="">
        <input type="checkbox"
            name="user_{{$userid=$user ? $user->id : 'new'}}_role_{{$rl->id}}"
            id="user_{{$userid}}_role_{{$rl->id}}"
            @if ($user)
                @if ($user->inRole($rl->slug))
                    checked="true"
                @endif
            @elseif (old('user_'.$userid.'_role_'.$rl->id) == "on")
                checked="true"
            @endif
            >
        {{$rl->name}}
    </label>
    @endforeach
</div>

上記のBladeファイルと、ユーザー新規ボタンをユーザービューに追加します。

  • resources/view/sentinel/users.blade.php をエディターで開く
  • ユーザー新規登録のブロックを以下のように修正する
            @if ($role=="admin")

            <form class="col" role="form" method="POST" action="{{url('users')}}">
                {{ csrf_field() }}

                <div class="panel panel-default">
                    <div class="panel-heading">ユーザー新規登録</div>
                    <div class="panel-body">

                        @include('parts.entry-user')

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4 help-block">
                            *パスワードを省略すると自動生成します。
                            </div>
                        </div>

                        <div class="form-group">
                            <label for="cb_user_new" class="col-md-4 control-label">ロール</label>
                            <div class="col-md-6" id="cb_user_new">
                                @include('parts.role-select', ['user' => false, 'roles' => $roles])
                            </div>
                        </div>

                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <button type="submit" class="btn btn-primary">
                                    <i class="fa fa-btn fa-user"></i> ユーザー登録
                                </button>
                            </div>
                        </div>

                    </div>
                </div>
            </form>

            @endif

以上で、ロールのチェックと、ユーザーの新規登録ボタンが追加されました。これで新規登録部分のビューは完成です。

ユーザー一覧ビュー

ユーザー一覧には以下の項目を表示します。

  • ユーザー名(first_name)
  • メールアドレス(email)
  • ロール(チェックボックスで示す)
  • 変更ボタン
  • 削除ボタン

パスワードは本人が[forgot password]で対応できるので、管理者は関与しないことにします。

変更時、削除時は、BootstrapのモーダルJavaScriptを使って、実行前に確認画面を表示します。

データ変更はPUT、削除はDELETEメソッドを利用します。BladeでPUTを実行するには、以下のように書きます。

<form action="{{ url('アクションURL') }}" method="POST">
{{ csrf_field() }}
{{ method_field('PUT') }}
<button type='submit'>PUT実行</button
</form>

以下に、一覧表示をするためのBladeコードを示します。

  • resources/views/sentinel/users.blade.php をエディターで開く
  • あらかじめ作成してあったユーザー一覧のテーブルの、<tbody>〜</tbody>を以下のように変更する
                <tbody>
                    @foreach($users as $user)
                        <tr>
                            <form action="{{ url('users/'.$user->id) }}" method="POST">
                                {{ csrf_field() }}
                                {{ method_field('PUT') }}

                                <td>
                                    <input type="text" name="user_{{$user->id}}_name" id="user_{{$user->id}}_name" value="{{$user->first_name}}" maxlength='255' size='20'>
                                </td>
                                <td>
                                    <input type="text" name="user_{{$user->id}}_email" id="user_{{$user->id}}_email" value="{{$user->email}}" maxlength='255' size='30'>
                                </td>
                                <td>
                                    @include('parts.role-select', ['user' => $user, 'roles' => $roles])
                                </td>
                                <td>
                                    <!-- Button trigger modal -->
                                    <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#modalUpdate-{{$user->id}}">
                                        <i class="fa fa-btn fa-refresh"></i>変更
                                    </button>

                                    <!-- 更新モーダル-->
                                    <div class="modal fade" id="modalUpdate-{{$user->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">&times;</span></button>
                                            <h4 class="modal-title">ユーザー情報を変更しますか?</h4>
                                          </div>

                                          <div class="modal-footer">
                                              <button type="submit" id="update-{{ $user->id }}" 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><!-- /.modal-content -->
                                      </div><!-- /.modal-dialog -->
                                    </div><!-- /.modal -->
                                </td>
                            </form>

                            <td>
                                <button type="button" class="btn btn-danger" data-toggle="modal" data-target="#modalDelete-{{$user->id}}">
                                    <i class="fa fa-btn fa-trash"></i>削除
                                </button>
                                <!-- 削除モーダル-->
                                <div class="modal fade" id="modalDelete-{{$user->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">&times;</span></button>
                                        <h4 class="modal-title">指定のユーザーを削除しますか?</h4>
                                      </div>
                                      <div class="modal-body">
                                          {{$user->first_name}} : {{$user->email}}
                                      </div>
                                      <div class="modal-footer">
                                          <form action="{{ url('users/'.$user->id) }}" method="POST">
                                              {{ csrf_field() }}
                                              {{ method_field('DELETE') }}

                                              <button type="submit" id="update-{{ $user->id }}" 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><!-- /.modal-content -->
                                  </div><!-- /.modal-dialog -->
                                </div><!-- /.modal -->

                            </td>
                        </tr>
                    @endforeach
                </tbody>

情報の変更をするには、古い情報と新しい情報を比較表示したり、変更がない場合はボタンが押せないなどの処理も欲しいところです。新旧のデータの比較はLaravelのold()関数を使えばできますが、ルートが増え、サーバーへのアクセスが発生するのであまりスマートな形になりません。Reactなどでフロントエンド側を作成した方が良さそうです。今回は、WebAPIと最低限のビューを揃えるだけにして、フロントエンドの構築は省きます。

拡張Userモデルの作成

Sentinelのデフォルトのユーザーモデルでは、ユーザーが持つ全てのロールをチェックしません。また、メールアドレスで管理者権限をチェックする機能も追加したいので、拡張したユーザーモデルを作成することにします。

UserExモデルの作成

新しいユーザーモデルであるUserExを作成します。このモデルには、 hasAccessRoles() という関数を追加します。使い方はhasAccess([パーミッションのリスト])と同様ですが、メールアドレスが管理者の場合は全てのアクセスを許可する機能と、このユーザーが持っている全てのロールのパーミッションをチェックする機能を持たせます。

  • ターミナルから、以下を実行
php artisan make:model UserEx
  • app/UserEx.php をエディターで開く
  • 以下のように書き換える
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Cartalyst\Sentinel\Users\EloquentUser;

class UserEx extends EloquentUser
{
    /**
     * 指定のパーミッションにアクセスできるかを確認する
     * @param array or string チェックするパーミッション名。
     */
    public function hasAccessRoles($args) {
        // 管理者メールアドレスチェック
        if ($this->email === config('app.admin_email')) return true;

        // 全てのロールでチェック
        foreach ($this->roles as $role) {
            if ($role->hasAccess($args)) {
                return true;
            }
        }
        return false;
    }
}

モデルの変更

Sentinelが参照するモデルを新しいものに差し替えます。

  • config/cartalyst.sentinel.php をエディターで開く
  • 'users'の宣言を以下のように修正
'users' => [

        'model' => 'App\UserEx',

    ],

以上で、新しく作成したUserExにモデルが差し代わり、 hasAccessRoles()が使えるようになりました。

管理者によるユーザー登録

ユーザー管理画面からのユーザー登録を実装します。

ミドルウェアの作成

ユーザー登録は、 user.create 権限を持つユーザーしか実行できません。処理する前に、その処理が実行可能かをチェックしたり、処理の後に事後処理をしたい場合に使うのがミドルウェア(Middleware)です。

Sentinelの権限チェックを利用して、ログインしているユーザーが指定のパーミッションを持っているかをチェックするミドルウェア Permission を作成します。

  • 以下をターミナルで実行して、Permissionという名前の新しいミドルウェアを作成
php artisan make:middleware Permission
  • app/Http/Kernel.php をエディターで開く
  • $routeMiddlewareの配列に、以下を追加する
        'permission' => \App\Http\Middleware\Permission::class,
  • app/Http/Middleware/Permission.php をエディターで開く
  • 以下のように修正する
<?php

namespace App\Http\Middleware;

use Closure;
use Sentinel;
use Redirect;

class Permission
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string $permission チェックするパーミッション
     * @return mixed
     */
    public function handle($request, Closure $next, $permission)
    {
        if (($user=Sentinel::check()) && $user->hasAccessRoles($permission)) {
            return $next($request);
        }
        return Redirect::back()->withInput()->withErrors(['permission' => trans('sentinel.permission_denied')]);
    }
}

以上で、権限チェック用のパーミッションの作成ができました。引数 $permission でチェックしたいパーミッションを指定できるので、このミドルウェア1つあれば、簡単な権限チェックは全て実行することができます。

これを UserController.phpコンストラクターに追加します。ミドルウェアの適用は、exceptだと指定の関数を外し、onlyだと指定の関数のみに適用させます。今回は個別に設定するので、 only を使います。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • 以下のuseを追加
use Sentinel;
use Redirect;
    /**
     * コンストラクター
     * 処理に権限チェックのミドルウェアを設定
     */
    public function __construct() {
        $this->middleware('permission:user.create', [
            'only' => [
                'store'
            ]
        ]);
    }

権限がない時のためのエラー文言を設定します。

  • resources/lang/ja/sentinel.php をエディターで開く
  • 以下を追加
    'permission_denied' => '権限がありません。',

エラーのビューをJavaScriptやその他の文言に対応できるように修正します。

  • resources/views/common/errors.blade.php をエディターで開く
  • 以下のように修正する
<!-- resources/views/common/errors.blade.php -->

<!-- Form Error List -->
<div id="error-block" class="alert alert-danger" @if (count($errors) == 0) style="display: none;" @endif >
    <strong>以下のエラーが発生しました。</strong>
    <br>
    <ul id="error-list">
        @foreach ($errors->all() as $error)
            <li>{{ $error }}</li>
        @endforeach
    </ul>
</div>

ユーザー管理画面に、エラー表示領域を追加します。

  • resources/views/sentinel/users.blade.php をエディターで開く
  • ファイルの先頭から、以下のようになる位置にエラーブロックをincludeする
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-12">

            @include('common.errors')

エラーが発生した時は、 Redirect::back()->withErros([]); でリターンすれば、上記の領域にエラーが表示されます。

以上で完了です。管理者以外でログインしたり、ログインしない状態で、 http://0.0.0.0:8080/test/admin/test にアクセスして、ユーザー登録をしようとすると権限がないとエラーが表示されて、処理がキャンセルされます。管理者でログインした状態で同様に操作すると、権限チェックに通るので処理が先に進みます。ただし、まだこの先を作っていないので白い画面が表示されるだけですが、それで成功です。

登録処理の実装

権限のチェックができたので、あとは実際にデータを登録する処理を追加します。必要な設定を追加します。

  • config/app.php をエディターで開く
  • 以下の設定を追加
    // 管理者名
    'admin_name' => env('ADMIN_NAME', '管理者'),

    // 自動生成するパスワードの長さ
    'password_generate_length' => 8,

メッセージを追加します。

  • resources/lang/ja/sentinel.php をエディターで開く
  • 以下の設定を追加
    'user_regist_subject' => 'ユーザー登録完了',
    'user_regist_done' => 'ユーザーの登録を完了しました。登録情報を管理者メールアドレスに送信しました。',

登録した内容をメールで管理者に登録するので、メールが使えるようにuseをつけます。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • 以下のuseを追加
use Mail;
  • あらかじめ作成されている store 関数を探して、以下のように処理を追加する
    public function store(Request $request)
    {
        // バリデーション
        $this->validate($request, [
           // nameは必須で、255文字まで
           'name' => 'required|max:255',
           // emailは必須で、emailの形式で、255文字までで、usersテーブル内でユニーク
           'email' => 'required|email|max:255|unique:users',
           // passwordは必須で、6文字以上255文字以下で、確認欄と一致する必要がある
           'password' => 'between:6,255|confirmed',
       ]);

       // パスワードが無指定の場合は、自動生成する
       $pass = $request->password;
       if (empty($pass)) {
           $pass = str_random(config('app.password_generate_length'));
       }

       // DBに登録
       $credentials = [
           'first_name' => $request['name'],
           'email' => $request['email'],
           'password' => $pass,
       ];
       $user = Sentinel::registerAndActivate($credentials);

       // ロールを設定する
       $roles = [];
       $rolenames = "";
       $allroles = Sentinel::getRoleRepository()->all();
       foreach($allroles as $role) {
            if ($request['user_new_role_'.$role->id] == "on") {
                $role->users()->attach($user);
                if (mb_strlen($rolenames) > 0) {
                    $rolenames .= ", ";
                }
                $rolenames .= $role->name;
            }
       }

       // メールで送信する
       $this->sendMail([
           'toemail' => config('app.admin_email'),
           'toname' => config('app.admin_name'),
           'subject' => trans('sentinel.user_regist_subject'),
           'blade' => 'sentinel.emails.user-regist-done',
           'args' => [
               'name' => $request['name'],
               'email' => $request['email'],
               'password' => $pass,
               'roles' => $rolenames,
           ]
       ]);

       // メールを確認して、承認してからログインすることを表示するページへ
       return redirect('users')->with('info', trans('sentinel.user_regist_done'));
    }
  • メールを簡単に送信するための関数も追加
    /**
     * 指定の内容でメールを送信する
     * @param array $params 送信データの連想配列
     * 'toemail' 宛先メールアドレス
     * 'toname' 宛先名
     * 'subject' メール件名
     * 'blade' 本文のテンプレート名
     * 'args' bladeに渡す連想配列
     */
    public function sendMail($params) {
        Mail::send($params['blade'], ['args' => $params['args']], function($m) use ($params) {
            $m->from(config('app.activation_from'), config('app.appname'));
            $m->to($params['toemail'], $params['toname'])->subject($params['subject']);
        });
    }

処理が完了した時のメッセージが表示できるように、情報の表示欄をユーザー一覧ビューに追加します。まずは、情報を表示するためのビューのパーツを作成します。

  • resources/views/parts/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
  • resources/views/sentinel/users.blade.php をエディターで開く
  • エラーの表示欄の上に以下を追加
@include('parts.info')

メールの雛形となるビューを作成します。

  • resources/views/sentinel/emails/user-regist-done.blade.php をエディターで開く
  • 以下を追加
以下の通り、ユーザーを登録しました。<br>
<br>
ユーザー名
  {{ $args['name'] }}<br>
メールアドレス
  {{ $args['email']}}<br>
パスワード
  {{ $args['password']}}<br>
ロール
  {{ $args['roles']}}<br>
<br>
<br>
--------<br>
[{{config('app.appname')}}]システムメール<br>
*本メールは送信専用のものです。返信には使えません。<br>
<br>

以上で管理者によるユーザー登録処理が完成です。 http://0.0.0.0:8080/test/admin/test でアクセスして、新規ユーザーの情報を入れて登録すると、実際にユーザーが登録されて、登録したユーザーの情報がメールで届くようになります。

一覧表示ルートの作成

ユーザーの登録ができるようになりましたので、実際のユーザー一覧のルートを組み込みます。

権限のチェック

ユーザー一覧の表示は、 user.view 権限を持つユーザーのみが可能なので、 UserController にミドルウェアを追加します。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • __construct()関数内に、以下を追加
        $this->middleware('permission:user.view', [
            'only' => [
                'index'
            ]
        ]);

ユーザー一覧を表示

一覧表示のためのコードをindexメソッドに追加します。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • 定義済みのindex()メソッドを探して、以下のようにコードを追加する
    public function index()
    {
        $user = Sentinel::check();

        return view('sentinel.users',
        [
            'role' => count($user->roles)==0 ? 'guest' : $user->roles[0]->slug,
            'roles' => Sentinel::getRoleRepository()->all(),
            'users' => Sentinel::getUserRepository()->all()
        ]);
    }

以上で、管理者やモデレーターユーザーを登録して、それぞれのユーザーでログインすれば、ユーザー管理ボタンを押してユーザー一覧を表示することができるようになりました。

ユーザーの削除

ユーザーを削除するボタンを機能させます。

権限のチェック

user.delete権限を持つユーザーだけに実行させるので、そのためのミドルウェアを追加します。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • __construct()関数内に、以下を追加
        $this->middleware('permission:user.delete', [
            'only' => [
                'destroy'
            ]
        ]);

削除に関連するメッセージの追加

ユーザーが見つからなかった時のエラーメッセージと、削除が完了した報告のメッセージを追加します。

  • resources/lang/ja/sentinel.php をエディターで開く
  • 以下のメッセージを追加
    'user_not_found' => '指定のユーザーが見つかりませんでした。',
    'user_delete_done' => '指定のユーザーを削除しました。',

削除処理を実装

UserControllerのdestroyメソッドに、指定のユーザーを削除するためのコードを実装します。ユーザーと関連するロールの設定は、リレーションを設定しているので自動的に削除されます。該当するユーザーだけ削除すれば良いので楽です。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • 予め作成されている destroy() メソッドを探して、以下のように書き換える
    public function destroy($id)
    {
        // ユーザーを検索
        $user = Sentinel::findById($id);
        if (!$user) {
            // 手動でアクセスした場合はユーザーが見つからない可能性があるので、チェックをしておく
            return Redirect::back()->withInput()->withErrors(['user_not_found' => trans('sentinel.user_not_found')]);
        }

        // 削除実行
        $user->delete();

        // 削除完了メッセージを添えて元のページに戻る
        return Redirect::back()->with(['info' => trans('sentinel.user_delete_done')]);
    }
}

更新処理

ユーザー管理の最後に、登録済みのユーザーの名前やメールアドレス、ロールの変更処理を実装します。

権限のチェック

user.update権限を持つユーザーだけに実行させるので、そのためのミドルウェアを追加します。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • __construct()関数内に、以下を追加
        $this->middleware('permission:user.update', [
            'only' => [
                'update'
            ]
        ]);

メッセージの追加

処理に伴うメッセージを追加します。

  • resources/lang/ja/sentinel.php をエディターで開く
  • 以下のメッセージを追加する
    'detach_role' => 'ロールを外しました。',
    'attach_role' => 'ロールを設定しました。',

更新処理を実装

UserControllerのupdate()メソッドに、更新処理を実装します。

  • app/Http/Controllers/Sentinel/UserController.php をエディターで開く
  • あらかじめ作成されている update()メソッドを探して、以下のようにコードを追加する
    public function update(Request $request, $id)
    {
        // ユーザーを検索
        $user = Sentinel::findById($id);
        if (!$user) {
            // 手動でアクセスした場合はユーザーが見つからない可能性があるので、チェックをしておく
            return Redirect::back()->withInput()->withErrors(['user_not_found' => trans('sentinel.user_not_found')]);
        }

        // 更新したデータがあれば更新する
        $userid = 'user_'.$id."_";
        $changed = [];
        if ((!empty($request[$userid.'name'])) && ($user->first_name !== $request[$userid.'name'])) {
            $changed['name_changed'] = $user->first_name." > ".$request[$userid.'name'];
            $user->first_name = $request[$userid.'name'];
        }
        if ((!empty($request[$userid.'email'])) && ($user->email !== $request[$userid.'email'])) {
            $changed['email_changed'] = $user->email." > ".$request[$userid.'email'];
            $user->email = $request[$userid.'email'];
        }

        if (count($changed) > 0)
        {
            $user->save();
        }

        // ロールのチェック
        $nowroles = "";
        foreach(Sentinel::getRoleRepository()->all() as $role) {
            $idxinrole = $userid.'role_'.$role->id;
            $inrole = (!empty($request[$idxinrole] && ($request[$idxinrole]==="on")));
            $nowrole = $user->inRole($role->slug);
            if ($nowrole && !$inrole) {
                // ロールを外す
                $changed['role_detach'.$role->id] = $role->name.trans('sentinel.detach_role');
                $role->users()->detach($user);
            }
            else if (!$nowrole && $inrole) {
                // ロールを設定
                $changed['role_attach'.$role->id] = $role->name.trans('sentinel.attach_role');
                $role->users()->attach($user);
            }
        }

        // 結果を表示して戻る
        return Redirect::back()->withInput()->with(['info' => $changed]);
    }

以上で、ユーザー管理画面は完成です。

app/Http/routes.phpに定義した Route::get('test/{role}/{routename}'・・・) のルートは不要になりましたので、削除してください。


ロールの管理はこちら

さくらインターネットで.htaccessを使ったリダイレクト

Web

さくらインターネットのスタンダードプランのレンタルサーバーで以下をやった時のメモです。

前提

この記事内容は、以下のような想定です。各所を自分の環境に置き換えて読んでください。

やりたいこと

ドメインの設定

さくらインターネットのサーバコントロールパネルのドメイン設定にて、以下を設定しておきます。

  • foo.sakura.ne.jpへのアクセスは、 /home/foo/www フォルダーに割り当て
  • foo.org へのアクセスは、 /home/foo/www/dokuji フォルダーに割り当て

www フォルダーの .htaccess

/home/foo/www/.htaccess を作成して、以下の内容にしました。

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /

    # http to https
    RewriteCond %{ENV:HTTPS} !^on$
    RewriteCond %{HTTP:X-Sakura-Forwarded-For} ^$
    RewriteRule .* https://foo.sakura.ne.jp%{REQUEST_URI} [R=301,L]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)/$ /$1 [L,R=301]

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_URI} ^/(.*)$
    RewriteRule .* https://foo.sakura.ne.jp/index.php/%1 [R,L]

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
</IfModule>

www/dokuji フォルダーの .htaccess

/home/foo/www/dokuji/.htaccess を作成して、以下の内容にしました。

<IfModule mod_rewrite.c>
    RewriteEngine On

    # Redirect root to wordpress
    RewriteCond %{REQUEST_URI} ^/$ [NC]
    RewriteRule .* http://%{SERVER_NAME}/wp [R=301,L]

    # Redirect HTTP App Access
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} ^/(.*)$
    RewriteRule .* https://foo.sakura.ne.jp/index.php/%1 [R=301,L]
</IfModule>

解説

各設定やRewriteの表記の意味についてです。

Rewriteルールの開始

まず冒頭の

<IfModule mod_rewrite.c>
    RewriteEngine On

によって、アクセスしてきたURLに対して、条件に合致する部分を書き換えを行う宣言をします。

RewriteRule

Rewriteの最重要コマンドです。パス指定に対して、最初のパラメーターに書き換えたい部分を表す正規表現を記載して、第2パラメーターで置き換えるパス表現を記載します。第3パラメーターは省略可で、書き換えた時の挙動やその後の処理を指定します。

RewriteCondやRewriteBaseは、このRewriteRuleを実行するかや、置き換えパスを省略するための付属の設定という位置付けです。

例えば

    RewriteRule .* https://%{SERVER_NAME}%{REQUEST_URI} [R=301,L]

と書いた場合、 ".*" は、あらゆる文字を0文字以上並べた文字列に一致する正規表現ですので、指定されたパスを全て次の第2パラメーターの文字列に書き換えることを表します。

第2パラメーターは、[https://]で開始して、%{SERVER_NAME}は環境変数で、指定されたパスのうちのドメイン名を得られます。%{REQUEST_URI}も環境変数で、"/"から始めたパスの部分が得られます。これにより、 http://foo.sakura.ne.jp/login などにアクセスがあった場合、以下のように書き換えられます。

https://foo.sakura.ne.jp/login

第3パラメーターは[]で囲まれた部分で、R=301は、ブラウザーのアドレスバーを書き換えた上で、恒久的なリダイレクトであることを表します。

Lは、このルールを適用したら、その後の.htaccessは無視する指定です。

後方参照

以下のようなルールの解釈です。

    RewriteRule ^(.*)/$ /$1 [L,R=301]

ルール内に()で囲まれた箇所があります。これは、その部分を後で参照できるようにするもので、書いた順に$1、$2、$3のように置換文字列の場所に記載できます。上記の例では()は1つだけなので、$1だけが利用できます。

上記は、最後が"/"で終わるパスだった場合、最後の"/"は削除するルールです。Laravelでルートを作成する際に、/で終わることを考慮しなくて済むようにする設定です。

- フラグ
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

というルールは、全てのパスに対して、URLエンコードを行う指定です。認可関連のアクセスについて、Laravelが自動生成したルールです。

RewriteBase

冒頭にある RewriteBase は、RewriteRuleの第2パラメーターの置き換えパスを相対パスで表記した時に、自動的に追加するパスを設定するものです。置き換えパスに相対パスを使わない場合は設定不要です。

RewriteCond

RewriteRuleは単体で使うこともできますが、その場合は条件に一致したパスは無条件で置き換えることになります。RewriteRuleの前にRewriteCondを使って、ルールを適用するかどうかの条件をつけることができます。

RewriteCondの第1パラメーターは、判定するデータです。第2パラメーターは、フラグや正規表現で、第1パラメーターに対するチェック内容を記載します。第3パラメーターは省略可能なフラグです。

RewriteCondは、直下のRewriteRule1つに適用されます。RewriteRule1行ごとに、必要なRewriteCondを記載します。

RewriteCondを複数並べた場合、第3パラメーターに[OR]を指定しなければ、並べた全ての条件が成立している必要があります(AND)。[OR]が指定されているRewriteCondは、それが成立していればRewriteRuleを実行します。

    RewriteCond %{ENV:HTTPS} !^on$
    RewriteCond %{HTTP:X-Sakura-Forwarded-For} ^$
    RewriteRule .* https://foo.sakura.ne.jp%{REQUEST_URI} [R=301,L]

上記がルール1つ分です。まずは、環境変数の%{ENV:HTTPS}が、"on"以外のものであることを確認しています。この環境変数には、httpsアクセスだった場合、"on"が入ります。ただし、さくらインターネットではこのパラメーターが効かない状況があるため、次のフラグを追加しています。

%{HTTP:X-Sakura-Forwarded-For}環境変数は、さくらインターネットの独自環境変数で、httpアクセスだった場合にIPアドレスが入ります。上記のテストでは"^$"となっています。これは1文字もないことを表す正規表現です。つまり、IPアドレスが入っていないので、httpアクセスではない(つまり、httpsである)ことを確認しています。

上記の2つのいずれも成立している時、httpアクセスなので、httpsアクセスに書き換えています。さくらインターネットでは共有SSLを使ったアクセスの場合、初期ドメインにアクセスすることが推奨されていますので、%{SERVER_NAME}ではなく、直接初期ドメインを書きました。

RewriteCondの後方参照

RewriteRuleと同様に、RewriteCondでも後方参照が使えます。指定の方法は同じで、指定箇所を()で囲みます。RewriteRuleで利用する場合は、%1などで指定します。

例えば、 http://foo.org/login などへのアクセスを、 https://foo.sakura.ne.jp/input.php/login に置き換えたい場合は以下のようにします。

    # Handle Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_URI} ^/(.*)$
    RewriteRule .* https://foo.sakura.ne.jp/index.php/%1 [R,L]

冒頭の2つの条件は、"!-d"でディレクトリーではないことを表し、 "!-f"でファイルでないことを表しています。つまり、存在しないURIへのアクセスを表しています。

3つ目の条件は、パス部分が "/???" という形式であることを条件にしています。最後の"(.*)"の部分が後方参照で利用できます。

書き換えルールは、 https://foo.sakura.ne.jp/index.php/%1 の形に置き換えることを表して、やりたいことを実現しています。

まとめ

一番はまったのは、.htaccessを書き換えても、ブラウザーなどに履歴が残っていると反映されないことでした。キャッシュをしない設定にしておくとか、ブラウザーを閉じてから再度アクセスするなどして確認してください。

さくらインターネットhttps判断は今の所以下で動いています。下の行だけでも動く感じでしたが、念のため2つ描いてあります。さくらの独自環境変数はいつ変更になるかわからないので注意が必要とのこと。また、最終的にはちゃんとSSLを取得して、独自ドメインに設定するべきかとは思います。

    RewriteCond %{ENV:HTTPS} !^on$
    RewriteCond %{HTTP:X-Sakura-Forwarded-For} ^$

Laravelの認証にSentinelを利用する

PHP Laravel Sentinel

(一通りチェックしました。ユーザー登録、アクティベーション、パスワードの再設定、ログインをSentinelに置き換える手順です。 2016/7/28)
(「認証用のビューを作る」は、ローカルで動くようにする手順の前に必要だったので、位置を入れ替えました。2016/8/22)

はじめに

PHPフレームワークLaravelには簡単な認証や認可ができるauthパッケージが組み込まれてはいますが、ユーザー登録時にメールの確認などなしにいきなり登録できてしまったり、認可はできるにはできますが自前のロジックでロールを判断しないといけないなど、本格的に利用するには機能が不足しています。

Sentinelは上記のような機能を持つPHPフレームワークで、Laravel5に組み込むための機能を提供してくれています。Sentinelが提供するのはロジックだけで、ビューやユーザー確認用のメールなどは自前で実装する必要がありますが、それでも手間は相当減らせます。

上記を組み合わせた環境を構築するメモです。

前提環境

  • Laravel5.2
  • Sentinel2.0
  • mac OS X 10.11.5
  • PHP5.6.19
  • MySQL5.7.11
  • composerは事前にインストールできているものとする
  • Postfixを設定して、macからgmailなどを通してメール送信できる環境が構築できている

Laravelのインストール

  • macでターミナルを起動
  • プロジェクトフォルダーを作るフォルダーに移動
  • laravel -v を実行して、コマンドが見つからなかったらいかを実行して、グローバル環境にLaravelをインストールする
composer global require "laravel/installer"
  • 以下で、Laravelのプロジェクトを作成して、中に入る
laravel new lara-sentinel
cd lara-sentinel

データベースを作成する

今回のサンプルで操作するためのデータベースを作成します。

  • MySQL にログインする
mysql -u ユーザー名 -p
  • 上記でMySQLにログインできない場合は、MySQLサーバーを起動
sudo mysql.server start
  • データベースを作成する。以下は、lara_sentinelというデータベースを作成する例
create database lara_sentinel;
  • 操作用のユーザーを作成して、権限を与えておく。以下は、user_lara_sentというローカルホスト上のユーザーを作成する例。パスワードは任意
CREATE USER 'user_lara_sent'@'localhost' IDENTIFIED BY 'YourPassword';
  • 作成したローカルホストのuser_lara_sentユーザーに、lara_sentinelデータベースへのアクセス権限を与える
GRANT ALL PRIVILEGES ON lara_sentinel.* TO 'user_lara_sent'@'localhost';

Laravelの環境設定

  • Atomなどのエディターで、上記で作成したlara-sentinelのプロジェクトフォルダーを開く
  • .envを開いて、各種設定を行う
    • APP_URLを、 http://0.0.0.0:8080 に変更
    • DB_DATABASEを、作成したデータベース名に変更。ここでは lara_sentinel
    • DB_USERNAMEを、作成したユーザー名に変更。ここでは、 user_lara_sent
    • DB_PASSWORDを、設定したパスワードに変更。任意に設定したパスワードを書く
    • メール設定をいかに差し替える
MAIL_DRIVER=smtp
MAIL_HOST=localhost
MAIL_PORT=25
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_NAME='送信元名'
MAIL_FROM_ADDRESS=from@mail.addr
MAIL_SENDMAIL='/usr/sbin/sendmail -bs'
MAIL_PRETEND=false

起動テスト

Laravelの起動を試します。

  • ターミナルで、プロジェクトフォルダーから以下を実行
php -S 0.0.0.0:8080 -t public

Sentinelをインストール

ここからは Sentinel の公式ページのインストール記事に従います。

  • プロジェクトフォルダー内から以下を実行してインストールする
composer require cartalyst/sentinel
  • config/app.php を開く
  • providers の配列を探して、その中に以下の行を加える
        /*
         * Sentinel Service Providers...
         */
        Cartalyst\Sentinel\Laravel\SentinelServiceProvider::class,
  • その下の aliases の配列に、以下を加える
        // Sentinel Aliases
        'Activation' => Cartalyst\Sentinel\Laravel\Facades\Activation::class,
        'Reminder'   => Cartalyst\Sentinel\Laravel\Facades\Reminder::class,
        'Sentinel'   => Cartalyst\Sentinel\Laravel\Facades\Sentinel::class,
  • 上書き保存する
  • ターミナルから以下を実行して、vendorフォルダーに必要なプロバイダーを出力する
php artisan vendor:publish --provider="Cartalyst\Sentinel\Laravel\SentinelServiceProvider"
  • 既存のユーザーテーブルは不要なので、マイグレーションで作成されないように以下のファイルを削除する
    • database/migrations/2014_10_12_000000_create_users_table.php
    • database/migrations/2014_10_12_100000_create_password_resets_table.php
  • 以下を実行して、データベースを生成する
php artisan migrate
  • ここでエラーが発生したら、.envを確認してデータベースの接続情報が合っているかや、MySQLサーバーが起動しているかを確認する

以上でSentinelの設置は完了です。 config/cartalyst.sentinel.php にSentinelの設定があるので、必要があれば変更します。

Webブラウザーhttp://0.0.0.0:8080 を再読み込みして、エラーが発生しなければここまで成功です。


認証用のビューを作る

手間を減らすために、認証関連のビューやコントローラーを既存のauth機能から取り込みます。すべて手製にするのであれば、この手順は不要です。

  • ターミナルからプロジェクトフォルダー内で以下を実行
php artisan make:auth

http://0.0.0.0:8080 を再読み込みすると、ビューが変更されて、ヘッダーに認証のためのボタンが並びます。Laravelのauth機能が組み込まれていますが、これをSentinelに差し替えていきます。


ローカルで実行できるように必要なものをダウンロードして組み込む

  • npmで必要なパッケージをsave-devで保存
npm install —-save-dev bootstrap font-awesome jquery@~2
  • publicフォルダー内に、vendorsフォルダーを作成
  • vendorsフォルダー内に、bootstrapフォルダーとjqueryフォルダーとfont-awesomeフォルダーを作成
  • node_modules/bootstrap/distフォルダー内のcssフォルダー、fontsフォルダー、jsフォルダーを、public/vendors/bootstrapフォルダーにコピー
  • node_modules/jquery/distフォルダー内のファイルを、public/vendors/jqueryフォルダーにコピー
  • node_modules/font-awesome/distフォルダー内の、cssフォルダーとfontsフォルダーを、public/vendors/font-awesomeフォルダーにコピー
  • resources/views/layouts/app.blade.php をエディターで開く
  • 以下の2行をコメントアウトして、ローカルのリンクに差し替える
{{--
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css" integrity="sha384-XdYbMnZ/QjLh6iI4ogqCTaIjrFk87ip+ekIjefZch0Y+PvJ8CDYtEs1ipDmPorQ+" crossorigin="anonymous">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:100,300,400,700">
--}}
<link rel="stylesheet" href="{{url('vendors/font-awesome/css/font-awesome.min.css')}}">
  • stylesは以下の通り
{{--
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
--}}
<link rel="stylesheet" href="{{url('vendors/bootstrap/css/bootstrap.min.css')}}">
<!-- JavaScripts -->
{{--
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js" integrity="sha384-I6F5OKECLVtK/BL+8iSLDEHowSAfUo76ZL9+kGAgTRdiByINKJaqTPH/QVNS1VDb" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
--}}
<script src="{{url('vendors/jquery/jquery.min.js')}}"></script>
<script src="{{url('vendors/bootstrap/js/bootstrap.min.js')}}"></script>

表示するメッセージを準備する

メッセージをハードコーディングするのを避けるために、言語ファイルを作成しておきます。

  • Finderなどでプロジェクトフォルダーを開く
  • resources/langフォルダーを開く
  • enフォルダーをコピーして、そのまま貼り付けて複製する
  • 「en のコピー」から、「ja」にフォルダー名を変更する
  • 両方のフォルダーに[sentinel.php]というファイルを作成しておく
  • config/app.php をエディターで開く
  • 'locale'を'en'から'ja'に変更する

以上で完了です。以降、sentinel.phpに追加した配列を、PHPのコードからは trans('sentinel.要素名')、bladeからは@lang('sentinel.要素名')で参照できます。


ユーザー登録

まずはユーザー登録をSentinelのものに入れ替えます。

Sentinelのユーザー登録についてはこちら

ここでは次のことをします。

  • ユーザー名と、メールアドレスと、パスワードと、確認用のパスワードを受け付ける
  • ユーザー名は1項目のみにして、first_nameを利用
  • ユーザー登録を完了するには、メールによる確認作業を必要にする
  • ユーザー名、メールアドレス、パスワードは必須
  • パスワードと確認用パスワードは一致していなければならない

ビューの調整

必要な項目は揃っているので変更しなくても構いません。表記を変更したり、表示を日本語にしたい、独自の欄を設けたい場合などの手順です。

  • resources/views/auth/register.blade.php をエディターで開く
  • 変更したい箇所を修正する
  • 上書き保存

コントローラーの作成

Sentinelを操作するコントローラーを作成します。app/Http/Controllers/Sentinelフォルダーの中に作成する例です。

  • ターミナルから以下を実行
php artisan make:controller Sentinel/SentinelController
  • ユーザー登録は、registerメソッドで行うことにする。app/Http/Controllers/Sentinel/SentinelController.phpをエディターで開いて、以下のようにする
<?php

namespace App\Http\Controllers\Sentinel;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\Http\Controllers\Controller;
use Sentinel;
use Mail;
use Activation;

class SentinelController extends Controller
{
    /**
     * ユーザー登録
     */
     protected function register(array $data)
     {
         // バリデーション
         $this->validate($request, [
            // nameは必須で、255文字まで
            'name' => 'required|max:255',
            // emailは必須で、emailの形式で、255文字までで、usersテーブル内でユニーク
            'email' => 'required|email|max:255|unique:users',
            // passwordは必須で、6文字以上255文字以下で、確認欄と一致する必要がある
            'password' => 'required|between:6,255|confirmed',
        ]);

        // 情報に問題がなければ、ユーザー登録
        $credentials = [
            'first_name' => $request['name'],
            'email' => $request['email'],
            'password' => $request['password'],
        ];
        $user = Sentinel::register($credentials);

        // アクティベーションを作成する
        $activation = Activation::create($user);
        // メールで送信する
        $this->sendActivationCode($user, $activation->code);

        // メールを確認して、承認してからログインすることを表示するページへ
        return redirect('login')->with('info', trans('sentinel.after_register'));
    }

    /**
     * 指定のユーザーに、指定のコードをメールで送信する
     * @param Cartalyst\Sentinel\Users\UserInterface $user ユーザー
     * @param string アクティベーションコード
     */
    private function sendActivationCode($user, $code) {
        Mail::send('sentinel.emails.activation', [
            'user' => $user,
            'code' => $code,
        ], function($m) use ($user) {
            $m->from(config('app.activation_from'), config('app.appname'));
            $m->to($user->email, $user->name)->subject(trans('sentinel.activate_title'));
        });
    }

}
  • 上書き保存

ルートの設定

コントローラーができたので、registerで呼び出したら登録するようにルートを設定します。ついでに、loginのルートも設定しておきます。

  • app/Http/routes.php をエディターで開く
  • Route::auth(); は、Laravelのauthは利用しないので削除
  • POSTメソッドのregisterで登録を発動させるので、Route::auth()があった場所に、以下を追加
Route::get('login', function() {return view('auth.login');});
Route::post('login', 'Sentinel\SentinelController@login');

Route::get('register', function() {return view('auth.register');});
Route::post('register', 'Sentinel\SentinelController@register');

登録後のメッセージを表示できるように、loginビューに表示領域を追加します。

  • resources/views/auth/login.blade.php をエディターで開く
  • <div class="panel-body"> という行の下に、以下を追加
                    @if (session('info') || isset($info))
                    <div class="alert alert-info">
                        @if (session('info'))
                            {{ session('info') }}
                        @endif
                        @if (isset($info))
                            {{ $info }}
                        @endif
                    </div>
                    @endif
                    @if (session('myerror') || isset($myerror))
                    <div class="alert alert-danger">
                        @if (session('myerror'))
                            {{ session('myerror') }}
                        @endif
                        @if (isset($myerror))
                            {{ $myerror }}
                        @endif
                    </div>
                    @endif

以上で、viewからはinfoとmyerror、redirectはsessionでinfoかmyerrorに情報を渡すことで、ログイン画面にメッセージを表示します。

メッセージの追加

エラーなどのメッセージを用意します。

  • resources/lang/ja/sentinel.php を開いて、以下を追加する
<?php
return [
    'activate_title' => 'ユーザー登録を完了してください',
    'after_register' => 'ご登録いただいたメールアドレスに、登録を完了するためのリンクを書いたメールを送信しました。メールを開いて、リンクを押して、ユーザー登録を完了させたら、以下からログインしてください。',
];
  • 上記のファイルを、enフォルダーにもコピーする

以上でルートとビューは完成です。あとは、メールを送信するために必要なデータを設定します。


メールの設定

ユーザーを有効にするアクティベーションにメールを使います。メールの送信はLaravelのMailクラスを使うと簡単にできます。

メール本文には、Bladeのビューを使います。Mail::send()メソッドの1つ目の引数は、メール本文のビューの名前。2つ目の引数は、ビューに渡す変数の連想配列、3つ目はメッセージのインスタンスを受け取るコールバック関数で、返信先や件名などをカスタマイズできます。

必要な設定を作成

  • config/app.php を開く
  • return []の中に、以下のようにアプリ名と送信元メールアドレスを追加
    'appname' => 'Laravel-Sentinelテスト',
    'activation_from' => env('ACTIVATION_FROM', 'from@email.com'),
  • ACTIVATION_FROMを環境ごとに変更する場合は、.env ファイルに定義を追加する

ビューを作成する

  • resources/views/sentinel/emails/activation.blade.php を作成
  • 以下のような本文を作成
{{ $user->first_name }}様<br>
<br>
[{{ config('app.appname') }}]へのユーザー登録を仮受付いたしました。<br>
以下のリンクをクリックして、登録を確定させてください。<br>
<br>
<a href="{{ $link = url('activate', [base64_encode($user->email), $code])}}">{{ $link }}</a><br>
<br>
--------<br>
[{{config('app.appname')}}]システムメール<br>
*本メールは登録専用のものです。返信には使えません。<br>
<br>

以上で、ユーザー登録の申請から、アクティベーション用のメールを送信するところまでできました。実行すると、登録した宛先にアクティベーション用のメールが送信されます。


アクティベーション処理

メールで送信した認証コードを使って、アクティベーションを行う処理を作成します。

アクティベーション用のルートを作成

メールからは、 activate/{メールアドレス}/{アクティベーションコード} という形式でアクティベーション用のアクセスがあります。これを受け取って、処理するためのルートを追加します。

  • app/Http/routes.php をエディターで開く
  • registerの登録の下に、以下を追加
Route::get('activate/{email}/{code}', 'Sentinel\SentinelController@activate');

アクティベーションの実行

ルートを定義したので、それを処理する activate メソッドを SentinelController.php に追加します。

ルート内のパラメーター({email}と{code})は、Requestに入れられて渡されますので、$request->emailや$request->codeのようにアクセスすることができます。

  • app/Http/Controller/Sentinel/SentinelController.php をエディターで開く
  • 以下の activate() メソッドをクラスに追加する
    /**
     * アクティベーション
     */
    protected function activate(Request $request) {
        // ユーザーを取得する
        $user = Sentinel::findByCredentials(['email' => base64_decode($request->email)]);
        if (is_null($user)) {
            return redirect('login')->with(['myerror' => trans('sentinel.invalid_activation_params')]);
        }

        // アクティベーション済みだった場合、そのまま戻る
        if (Activation::completed($user)) {
            return redirect('login');
        }

        // アクティベーションを実行する
        if (!Activation::complete($user, $request->code)) {
            return redirect('login')->with(['myerror' => trans('sentinel.invalid_activation_params')]);
        }

        return redirect('login')->with(['info' => trans('sentinel.activation_done')]);
    }

必要なメッセージの追加

アクティベーション関連のメッセージを追加します。

  • resources/lang/enフォルダーと、resources/lang/jaフォルダー内のsentinel.phpに、以下を追加
    'invalid_activation_params' => 'アクティベーションの情報が一致しませんでした。',
    'activation_done' => 'ユーザー登録を完了しました。ログインして、サービスをご利用ください。',


以上で、アクティベーション処理は完成です。メールに届いたアクティベーションのリンクをクリックすると、アクティベーションが完了して、ログインできるようになります。また、 http://0.0.0.0:8080/activate/bademail/badcode などでアクセスすると、アクティベーションのパラメーターが不正である旨、表示されます。


ログイン処理の実装

アクティベーションが完了したら、ログインができるようになります。ログイン処理を、Sentinelに変更します。ルートは前の手順ですでに作成済みなので、postに対するコントローラーの作成を行います。

必要なメッセージの追加

ログインに関連するメッセージを追加します。

  • resources/lang/ja/sentinel.php をエディターで開く
  • 以下のメッセージを追加
    'not_activation' => 'ユーザー登録が完了していません。登録したメールアドレスに、登録確認用のメールを送信してあります。メールを開いて、リンクをクリックしてください。メールを紛失した場合は、下のリンクからメールを再送できます。',
    'login_failed' => 'ログインに失敗しました。正しいメールアドレスとパスワードでログインしてください。',
    'login_throttling' => 'ログイン失敗が規定回数を越えました。一定時間、IPを凍結します。',

メニューをログイン・ログアウトに合わせて変更

ログイン実装に先立って、ヘッダーメニューがログインしているかどうかに応じて切り替わるようにしておきます。切り替え処理は app.blade.php 内で行っているので修正します。

  • resources/views/layouts/app.blade.php をエディターで開く
  • @if (Auth::guest()) という行を探して、以下に書き換える
                    @if (Sentinel::guest())
  • {{ Auth::user()->name }} という箇所を探して、以下に書き換える
{{ Sentinel::getUser()->first_name }}

以上で完了です。ログインしたらメニューが切り替わるのが確認できるようになりました。

ログイン処理の実装

ログイン処理を実装します。アクティベーションされていない時や、ログイン失敗時の処理もまとめて作成します。アクティベーションされていない時には、アクティベーションコードの再送信をする指示をビューに投げていますが、この辺りの実装は後ほど行います。

  • app/Http/Controllers/Sentinel/SentinelController.php をエディターで開く
  • アクティベーション時と、ログイン失敗による凍結を確認するために、以下のuseを冒頭に追加する
use Cartalyst\Sentinel\Checkpoints\NotActivatedException;
use Cartalyst\Sentinel\Checkpoints\ThrottlingException;
  • ログインが成功した時に表示する先のパスを、クラス内に定義する。以下は、ログインしたらルートパスを表示するもの。適宜、変更すること
    /**
     * ログイン後に表示するパス
     *
     * @var string
     */
    protected $redirectTo = '/';
  • 以下のloginメソッドをSentinelControllerクラスに追加
    /**
     * ログイン
     */
    protected function login(Request $request) {
        // バリデーション
        $this->validate($request, [
            'email' => 'required|email|max:255',
            'password' => 'required|between:6,255',
            'remember' => 'boolean',
        ]);

        // 認証処理
        try {
            $this->userInterface = Sentinel::authenticate([
                'email' => $request['email'],
                'password' => $request['password']
            ], $request['remember']);
        } catch (NotActivatedException $notactivated) {
            return view('auth.login', [
                'myerror' => trans('sentinel.not_activation'),
                'resend_code' => $request['email'],
            ]);
        } catch (ThrottlingException $throttling) {
            return view('auth.login', ['myerror' => trans('sentinel.login_throttling')."[あと".$throttling->getDelay()."秒]"]);
        }

        if (!$this->userInterface) {
            // エラー
            return view('auth.login', ['myerror' => trans('sentinel.login_failed')]);
        }

        return redirect($this->redirectTo);
    }

以上で、ログイン、未アクティベーション、IP凍結に対応しました。あとは、未アクティベーション時に、再度アクティベーションコードをメールする処理を追加します。

アクティベーションコードの再送信

ユーザーが誤ってアクティベーションコードを削除した場合を想定して、アクティベーションコードを再送信する機能を追加します。約束事は以下の通りです。

以下、実装手順です。

  • app/Http/routes.php をエディターで開く
  • Route::get('register', ・・・);の上に、以下のコードを追加する
Route::get('register/{email}', 'Sentinel\SentinelController@resendActivationCode');
  • app/Http/Controller/Sentinel/SentinelController.php をエディターで開く
  • 以下の関数を、クラスに追加する
    /**
     * 指定のメールアドレスのアクティベーションコードを再送する
     */
    protected function resendActivationCode(Request $request) {
        // 古いアクティベーションコードを削除
        Activation::removeExpired();

        // ユーザーを確認
        $user = Sentinel::findByCredentials(['email' => base64_decode($request->email)]);
        if (is_null($user)) {
            return redirect('login')->with(['myerror' => trans('sentinel.invalid_activation_params')]);
        }

        // すでにアクティベート済みの時は、何もせずにログインへ
        if (Activation::completed($user)) {
            return redirect('login')->with(['info' => trans('sentinel.activation_done')]);
        }

        // アクティベーションの状況を確認
        $exists = Activation::exists($user);
        if (!$exists) {
            // 存在しない場合は、再生成して、そのコードを送信する
            $activation = Activation::create($user);
        }
        else {
            // 現在のコードを
            $activation = $exists;
        }

        // メールで送信する
        $this->sendActivationCode($user, $activation->code);
        // メールを確認して、承認してからログインすることを表示するページへ
        return redirect('login')->with('info', trans('sentinel.after_register'));
    }

ビューの調整

アクティベーションが済んでいない時に、アクティベーションコードを再送信するためのリンクを表示するようにします。アクティベーションの再送信は、

  • resources/views/auth/login.blade.php をエディターで開く
  • パスワード欄の下に、以下のコードを追加
                        @if(isset($resend_code))
                        <div class="form-group">
                            <div class="col-md-6 col-md-offset-4">
                                <a class="btn btn-link" href="{{ url('register', base64_encode($resend_code))}}">
                                    ユーザー登録を完了させるメールを再送
                                </a>
                            </div>
                        </div>
                        @endif

以上で完了です。ユーザー登録をした後、アクティベーションをメールで行う前に、ログインを試してみてください。アクティベートがみ完了である旨、メッセージが表示されて、アクティベートコードの再送信のリンクが表示されます。リンクをクリックすると、アクティベートコードが再送信されます。その時点で、アクティベートコードが古くなっていたら、新しいアクティベートコードを生成して、そのコードをメールします。


パスワードリセットをSentinelに置き換える

login画面のパスワードリセットに対応します。

ルートの作成

パスワードリセット画面の呼び出しと、パスワードリセットを実行するPOSTルートを定義します。

  • app/Http/routes/php をエディターで開く
  • registerルートの下辺りに、以下を追加
Route::get('password/reset/{email}/{code}/{password}', 'Sentinel\SentinelController@resetPassword');
Route::get('password/reset', function() {return view('auth.passwords.reset', ['token'=>'']);});
Route::post('password/reset', 'Sentinel\SentinelController@sendResetPassword');

リマインダーに必要なメッセージを追加

リマインダーに関連する文言をメッセージに追加しましょう。

  • resources/lang/ja/sentinel.php をエディターで開く
  • 以下のメッセージを配列に追加する
    'password_reset_sent' => 'パスワードを再設定するためのメールを送信しました。届いたメールに記載のリンクをクリックするとパスワードの再設定が完了しますので、新しいパスワードでログインしてください。',
    'reminder_title' => 'パスワードをリセットできます。',
    'password_reset_done' => 'パスワードをリセットしました。',
    'password_reset_failed' => 'リセットコードが古くなっていました。もう一度、リセットし直してください。',

メールのビューを作成

パスワードをリセットするためのメールの本文を作成します。

  • resources/views/sentinel/mails/reminder.blade.php を作成して、以下のコードを書く
{{ $user->first_name }}様<br>
<br>
[{{ config('app.appname') }}]のパスワードを再設定するには、以下のリンクをクリックしてください。<br>
<br>
<a href="{{ $link = url('password/reset', [base64_encode($user->email), $code, base64_encode($password)])}}">{{ $link }}</a><br>
<br>
--------<br>
[{{config('app.appname')}}]システムメール<br>
*本メールは登録専用のものです。返信には使えません。<br>
<br>

パスワードをリセットするメソッドの作成

  • app/Http/Controllers/Sentinel/SentinelController.php をエディターで開く
  • SentinelのReminderを使えるように、冒頭にuseを追加
use Reminder;
  • クラス内に、以下のコードを追加
    /**
     * パスワードを再設定するための処理
     * ユーザーが有効で、パスワードが条件に合致していたら、SentinelのReminderを使って処理する
     */
    protected function sendResetPassword(Request $request) {
        // 古いリマインダーコードを削除
        Reminder::removeExpired();

        // チェック
        $this->validate($request, [
           // emailは必須で、emailの形式で、255文字まで
           // メールアドレスの有無は、不正を避けるためにチェックしない
           'email' => 'required|email|max:255',
           // passwordは必須で、6文字以上255文字以下で、確認欄と一致する必要がある
           'password' => 'required|between:6,255|confirmed',
       ]);

       // ユーザーを検索
       $user = Sentinel::findByCredentials(['email'=>$request->email]);
       if (is_null($user)) {
           // ユーザーがいなければ成功したような感じにしてログイン画面へ
           return redirect('login')->with(['info'=>trans('sentinel.password_reset_sent')]);
       }

       // リマインダーが作成済みなら、それを再送信する
       $code = "";
       $exists = Reminder::exists($user);
       if ($exists) {
           // すでに設定されているので、リマインダーコードを設定
           $code = $exists->code;
       }
       else {
           // 新規にリマインダーを作成して、コードを返す
           $reminder = Reminder::create($user);
           $code = $reminder->code;
       }

       // メールを送信
       Mail::send('sentinel.emails.reminder', [
           'user' => $user,
           'code' => $code,
           'password' => $request->password,
       ], function($m) use ($user) {
           $m->from(config('app.activation_from'), config('app.appname'));
           $m->to($user->email, $user->name)->subject(trans('sentinel.reminder_title'));
       });

       // 成功したら、login画面へ移動
       return redirect('login')->with(['info'=>trans('sentinel.password_reset_sent')]);
    }

バリデーションは以下に対して行います。

  • 空欄
  • パスワードが6文字以上、255文字以下じゃない
  • パスワードと確認が一致しない
  • メールの有無を確認するとメールアドレスを探索される可能性があるのでチェックしない

以上で、パスワードをリセットするメールの送信まで出来ました。 http://0.0.0.0:8080/login にアクセスして、[Forgot Your Password?]をクリックして、Reset Passwordの画面に移動します。この画面はそのまま利用できます。emailと新しく設定するパスワードを入力してボタンを押すと、メールが送信されます。

リマインダーの実行

メールで送られたリンクをクリックした時の処理を実装して完成させます。

  • app/Http/Controllers/Sentinel/SentinelController.php をエディターで開く
  • 以下のresetPasswordメソッドをクラスに追加する
    /**
     * パスワードのリセットを実行する
     */
    protected function resetPassword(Request $request) {
        // データ長を調整しておく
        $email = substr(base64_decode($request->email), 0, 255);
        $code = substr($request->code, 0, 64);
        $passwd = substr(base64_decode($request->password), 0,255);

        $user = Sentinel::findByCredentials(['email' => $email]);
        if (is_null($user)) {
            // 不正なアクセスだが、正常に終わったようなメッセージを返す
            return redirect('login')->with('info', trans('sentinel.password_reset_done'));
        }

        // リマインダーを完了させる
        if (Reminder::complete($user, $code, $passwd)) {
            // 成功
            return redirect('login')->with('info', trans('sentinel.password_reset_done'));
        }

        // 失敗
        return redirect('login')->with('info', trans('sentinel.password_reset_failed'));
    }

ルートはすでに設定しているので、以上で完成です。メールに届いたリンクをクリックして、パスワードが変更されることを確認してください。


ログアウト

ログアウトに対応させます。

ルートの追加

ログアウトのルートを設定します。

  • app/Http/routes.php をエディターで開く
  • 以下のルートを適当な場所に追加する
Route::get('logout', 'Sentinel\SentinelController@logout');

コントローラーを追加する

  • app/Http/Sentinel/SentinelController.php
  • logoutメソッドをクラスに追加する
    /**
     * ログアウト処理
     */
    protected function logout(Request $request) {
        Sentinel::logout();

        return redirect($this->redirectTo);
    }

以上でログアウトの実装完了です。


Sentinel用の認可ミドルウェアを提供

ログインしているのに、Homeを表示しようとするとログイン画面に移行してしまいます。これは、ログインしているかどうかのチェックがまだSentinelに対応していないためです。Laravel Authの認証ミドルウェアを、Sentinelのものに書き換えてログインに対応します。

Authのguard(毎回、認証を要求するアカウント)は今回は利用の予定がなかったので、シンプルにログインしているかのチェックのみにします。

  • app/Http/Middleware/Authenticate.php をエディターで開く
  • Sentinelにアクセスできるように以下のuse文を追加
use Sentinel;
  • handleメソッドを以下のように書き換える
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (Sentinel::guest()) {
            if ($request->ajax() || $request->wantsJson()) {
                // ajaxでのアクセスや、JSONの場合はテキストのみ
                return response('Unauthorized.', 401);
            } else {
                // ログインしていないのでログイン画面へ
                return redirect()->guest('login');
            }
        }

        // 認証しているので指定のルートへ
        return $next($request);
    }

以上で対応できました。ログインすると、Homeにアクセスできるようになります。

コントローラーの__construct()で $this->middleware('auth'); と書いておくと、そのコントローラーの操作は、Sentinelでログインしていないとログイン画面にリダイレクトされるようになります。

ついでに、RedirectIfAuthenticated ミドルウェアもSentinelに対応させておきましょう。

  • app/Http/Middleware/RedirectIfAuthenticated.php をエディターで開く
  • Sentinelにアクセスできるように以下のuse文を追加
use Sentinel;
  • handleメソッドを以下のように修正する
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @param  string|null  $guard
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (Sentinel::check()) {
            return redirect('/');
        }

        return $next($request);
    }

上記は、 app/Http/Kernel.php の$routeMiddleware 配列に guest というミドルウェア名で定義されています。

      • -


以上で、Laravelの認証の機能を全て置き換えることができました。ユーザー登録にメールによる確認が必要になるのはメリットが大きいです。

編集可能なロールやパーミッションを使った認可機能がつけば、さらにSentinelに変更した効果が高まります。こちらに記事をまとめました。

Reactでのbrowser.js(browser.min.js)が見つからない

JavaScript React Web

Reactのサンプルで、CDNから browser.min.js を読み込んでいます。ローカルで動かそうと思って、index.htmlで読み込んでいるスクリプトを用意しようとしたら、 browser.min.js は、Babel6で削除されたとか言われて入手が困難です。

browser.jsとは?

browser.jsは、text/babelタイプを解釈して、ブラウザーサイドでJSXをビルドするのに利用するものだそうです。僕はすでにBabelを使ってReactのビルドはしていたので、browser.jsは不要でした。

解決策

index.html内でReact関連のスクリプトを読むところの"text/babel"を"text/javascript"に変更すれば、browser.min.jsやらbrowser.jsは読み込まなくても動作します。

react.min.js、react-dom.min.js、jquery.min.jsは、npmでインストールすれば、インストール先のdistフォルダーなどに見つかりますので、それを参照できる場所にコピーすれば動きます。

なお、remarkable.jsはマークダウンを使うためのものなので、マークダウンを使わないなら不要です。