Laravel4、ロールと許可ベースのルート制御(3)

タグ: Laravel4  

この記事は以下の続きです。

後は、フィルターとルートを定義すれば良いのですが、この手のチュートリアルでは、メッセージが表示されないと、動作がわかりづらくなります。

主題からは外れてしまいますが、メッセージを表示できるように準備しておきます。

app/views/masterView.blade.php

<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>@yield('title')</title>
    </head>
    <body>
        <h1>@yield('title')</h1>
        <div id="messages">
            @if ( $message )
            <p style="color:green;">{{ $message }}</p>
            @endif
            @if ( $caution )
            <p style="color:red;">{{ $caution }}</p>
            @endif
            @if ( $warning )
            <p style="color:blue;">{{ $warning }}</p>
            @endif
        </div>
        @yield('content')
    </body>
</html>

このmasterViewに対する、ビューコンポーサーを追加しましょう。本来は、サービスプロバイダーが指定するためには最適ですが、チュートリアルが更に長くなるため、今回はapp/start/global.phpの最後に追加してください。

View::composer( 'masterView', function($view)
    {
        $view->message = isset( $view->message ) ? $view->message : Session::get( 'message', false );
        $view->caution = isset( $view->caution ) ? $view->caution : Session::get( 'caution', false );
        $view->warning = isset( $view->warning ) ? $view->warning : Session::get( 'warning', false );
    } );

使用するビューを2つ作成しておきましょう。まずは、ログイン用のビューです。

app/views/auth/login.blade.php

@extends('masterView')

@section('content')
{{ Form::open() }}

{{ Form::label('username', 'ユーザー名:') }}
{{ Form::text('username', Input::old('username', '')) }}
@if ($errors->has('username'))
<p style="color:red;">{{ $errors->first('username') }}</p>
@endif
<br>
{{ Form::label('password', 'パスワード:') }}
{{ Form::password('password') }}
@if ($errors->has('password'))
<p style="color:red;">{{ $errors->first('password') }}</p>
@endif
<br>
{{ Form::submit('ログイン') }}

{{ Form::close() }}
@stop

続いて、汎用の出力ページです。$contentの中身を表示するビューです。

Laravelで一番簡単にページ出力を行おうとするなら、クロージャールートや、コントローラーのアクションメソッドから、出力文字列をreturnするだけで済みます。しかし、これですと、メッセージが出力されません。無理に処理を入れると煩雑になります。そのため、ビューの$contentに出力内容を渡し、表示する汎用ページを使用します。

app/views/showContent.blade.php

@extends('masterView')

@section('content')
{{ $content }}
@stop

単純なページです。

ルーティング

さて、いよいよルートの指定に入ります。今回は、コントローラーとクロージャーによるルートの両方を使用しています。

app/route.phpを次のように書き換えます。

<?php

Route::get( '/', function()
    {
        return View::make( 'showContent' )
                ->with( 'content', 'ホームページです。' );
    } );

Route::get( 'login', array(
    'uses' => 'AuthController@showLoginPage',
    'as' => 'login',
) );

Route::post( 'login', array(
    'uses' => 'AuthController@handleLoginInputs',
    'before' => 'csrf'
) );

Route::get( 'logout', array(
    'uses' => 'AuthController@handleLogout',
    'as' => 'logout'
) );

Route::get( 'super', array(
    'as' => 'super-area',
    'before' => 'Super',
    function ()
    {
        return View::make( 'showContent' )
                ->with( 'content', 'スーパーユーザーエリア' );
    }
) );

Route::get( 'roles', array(
    'uses' => 'RoleController@showRoleList',
    'as' => 'roles-index',
    'before' => 'AdminAreaAccess',
) );

Route::get( 'post/new', array(
    'as' => 'new-post',
    'before' => 'NewPost',
    function ()
    {
        return View::make( 'showContent' )
                ->with( 'content', '新規ポスト作成' );
    }
) );

Route::get( 'post/edit/{id}', array(
    'as' => 'edit-post',
    'before' => 'EditPost',
    function($id)
    {
        return View::make( 'showContent' )
                ->with( 'content', '既存ポスト(ID:$id)編集' );

        return "";
    }
) )->where('id', '[0-9]+');

ルートの定義自体はLaravel4の新しい機能をあまり利用していません。プレースホルダー{id}を束縛する方法が変更になっている程度でしょう。

これでも読めるとは思いますが、定義されているルートをお手軽に確認したい場合、コマンドラインから行うことも可能です。

php artisan route

ルーティングで使用しているBeforeフィルターのうち、csrf以外は、このチュートリアルのために作成したものです。

フィルターの定義はapp/fileters.phpで行います。最後に、以下のコードを追加してください。

// 本来、グローバル関数より、
// クラスメソッドにしたほうが、好ましい

function allowPermission()
{
    // ログインしていなければ、許可なし
    if( Auth::guest() ) return false;

    $args = func_get_args();

    // ログイン中のユーザーの役割とパーミッションをまとめて取得
    // Eagerローディングされているため、以降はDBアクセスは起きない
    // この一文で3回クエリーが発行される。
    $user = User::with( 'roles.permissions' )->find( Auth::user()->id );

    // superユーザーとbanされたユーザーを先にチェック
    $roles = $user->roles;

    foreach( $roles as $role )
    {
        if( $role->rolename == 'super' ) return true;
        if( $role->rolename == 'ban' ) return false;
    }

    // 引数に指定された許可を持っているか調べる
    foreach( $user->roles as $role )
    {
        foreach( $role->permissions as $permission )
        {
            if( in_array( $permission->allow, $args ) ) return true;
        }
    }

    return false;
}

// 追加フィルター

Route::filter( 'super', function()
    {
        if( !allowPermission( 'Super' ) )
        {
            return Redirect::guest( 'login' )
                    ->with( 'warning', '特権ユーザー権限がありません。' );
        }
    } );

Route::filter( 'AdminAreaAccess', function()
    {
        if( !allowPermission( '管理者エリア読み書き' ) )
        {
            return Redirect::guest( 'login' )
                    ->with( 'warning', '管理者権限がありません。' );
        }
    } );

Route::filter( 'NewPost', function()
    {
        if( !allowPermission( '記事投稿' ) )
        {
            return Redirect::guest( 'login' )
                    ->with( 'warning', '記事投稿権限がありません。' );
        }
    } );

Route::filter( 'EditPost', function($route)
    {
        if( allowPermission( '記事修正' ) ) return;

        // Routeクラスが渡されてくるので、
        // プレースホルダー{id}の場所に指定された
        // ポスト番号を取得
        $postId = $route->getParameter( 'id' );

        if( allowPermission( '自投稿編集' ) )
        {
            // ここで本来、$postIdの投稿がログインユーザーにより
            // 作成されているかチェックする。
            // 作成されていた場合は、そのままreturnする。
            // 今回は、サンプルのため7のみOKとする。
            if( $postId == '7' ) return;
        }

        return Redirect::guest( 'login' )
                ->with( 'warning', "記事ID: $postId に対する記事修正権限がありません。" );
    } );

フィルターを含め、任意の場所から使用者が特定の許可を持っているかチェックするためのヘルパー関数、allowPermissionを使っています。コメントにも書きましたが、本来はクラスメソッドとして実装したほうが良いでしょう。

フィルターは4つ定義時しています。permissionsテーブルに定義した許可を全部フィルターにしていません。他のものも、同じパターンで記述できます。チュートリアルですから、残りのフィルターを定義したり、それをチェックするルートを増やしたり、ユーザーに付ける役割、役割に持たせる許可を変更し、自分でも試してください。

再度繰り返しますが、チュートリアルですから、完璧に動作するものではありません。実用的には、既に存在するパッケージを利用したほうが無難です。あくまで、Laravelのフィルターによるルートの制御の能力を学んでもらうのが、目的です。

allowPermission()の中で注目してもらいたいのは、EloquentのEagerローディングを使用し、ユーザーに付けられた役割と、それらの役割が持っている許可を一文で取得しています。つまり、$userはユーザーの情報だけでなく、役割と許可の情報も取り込んでいます。

この一文で、3つのクエリーが実行されます。しかし、それ以降の処理ではテーブルに対して、クエリーは実行されません。Eagerロードされた情報のみにアクセスしているからです。

Beforeフィルターの中から、値をリターンした場合、そのルートの実行は中止され、代わりにリターンした値がレスポンスとして使用されます。return文だけで、何も値を返さない場合、フィルターをパスしたものとして扱われ、そのルートは実行されます。

EditPostフィルターはやや複雑になっています。今回のチュートリアルでは、投稿記事のpostsテーブルは作成しませんので、内容は一部ダミーコードになっています。

「記事修正」の許可は、全記事に対する編集を許します。この許可を持っていれば、今編集しようとしている記事が誰のものであっても、編集可能としています。

「自投稿編集」は、ログイン中のユーザーが投稿した、自分の記事のみの編集を許します。そのため、本来は、URI中のプレースホルダー{id}で指定された記事が、ログイン中のユーザーのものかをチェックしなくてはなりません。今回は、ポストIDが7の場合のみ、ログインユーザーのものであるとしています。

ここで注目してもらいたいのは、URI中のプレースホルダー{id}にあたる数値を取得するために、$route->getParameter( 'id' )としていることです。ドキュメントを確認してもらえば、フィルターには3つのパラメーターが渡されます。今回は最初の$routeを使用します。これは、LaravelのRouteクラスです。このクラスが用意しているgetParameterメソッドを使用しています。

URIの並びに基づいて、セグメントの3つ目として取得すると、ルートのURIを変更した時にフィルターが動作しなくなります。柔軟性を持たせるため、getParameterを使用しましょう。

どのフィルターが適用され、リダイレクトされたのかをわかりやすくするため、メッセージを変えてあります。しかし、実用にするには、いささか中途半端です。不特定多数のユーザーが使用するシステムであれば、何の権限が必要かを指摘する必要はありません。逆に、閉じたシステムの場合なら、もう少しユーザーよりの分かりやすいメッセージにしましょう。メッセージ内容も、チュートリアルのためのものです。

さて、残りはコントローラーです。2つ使用しています。

認証を担当するAuthController.phpコントローラーです。

<?php

class AuthController extends BaseController
{

    public function showLoginPage()
    {
        return View::make( 'auth.login' );
    }

    public function handleLoginInputs()
    {
        $rules = array(
            'username' => array( 'required' ),
            'password' => array( 'required' ),
        );

        $inputs = Input::only( 'username', 'password' );

        $val = Validator::make( $inputs, $rules );

        if( $val->fails() )
        {
            return Redirect::back()
                    ->withErrors( $val )
                    ->withInput()
                    ->with('warning', '入力を修正してください。');
        }

        if( Auth::attempt( $inputs ) )
        {
            return Redirect::intended( '/' )
                ->with('message', 'ログインしました。');
        }

        return Redirect::back()
            ->withInput()
            ->with('warning', 'ユーザー名とパスワードを正しく入力してください。');
    }

    public function handleLogout()
    {
        Auth::logout();

        return Redirect::to('/')
            ->with('message', 'ログアウトしました。');
    }

}

つぎに、RoleController.phpです。まあ、こちらはスタブですね。

<?php

class RoleController extends BaseController
{

    public function showRoleList()
    {

        $content = '';
        $roles = Role::all();

        foreach($roles as $role) {
            $content .= $role . '<br>';
        }

        return View::make('showContent')
            ->with('content', $content);
    }

}

フィルターにより、そのルートの実行が適切なのかをチェックするコードを独立させることが可能です。

Laravelのフィルターは、強力かつ柔軟です。ですから、フィルターのコーディング自身が複雑になり得ます。そのためLaravel4では、フィルターをクラスとして定義することもできるようになっています。ユニットテストを実行可能にするためです。

まあ、腕試しに本格的なACL風リソース制御を作るも良し、フィルターの柔軟性を認識して、もっとお手軽なルーティング制御にするも良し、自由にBeforeフィルターを活用ください。