PHP
laravel
LaravelDay 13

laravel4 綺麗なcontrollerとcustomValidate

More than 3 years have passed since last update.

今週来週と無駄に4回も書こうと思いますが、お付き合い下さいませ。

今回はlaravel4初心者向けのtipsです。
modelでvalidateしたい場合は他の方が書いてくれてますので、そちらを参考にしてください。

controller

laravel4ではcontrollerを下記の様に記載していきます(1例)

controller
use Model\User;

class UserController extends \BaseController
{

    const _CREATE_USER_KEY = '_create_user:';

    public $userValidateRules = [
        'username' => 'required',
        'password' => 'required',
        'email' => 'required|email'
    ];
    //
    public $userValidateMessages = [
        'username.required' => 'アカウント名を入力してください',
        'password.required' => 'パスワードを入力してください',
        'email.required' => 'メールアドレスを入力してください',
        'email.email' => 'メールアドレス正しく入力してください'
    ];

    /**
     * @param User $user
     */
    public function __construct(User $user)
    {
        $this->beforeFilter('csrf', ['on' => 'post']);
        $this->beforeFilter('auth');
        $this->_users = $user;
    }

    // 登録
    public function pageForm()
    {
        \Session::put(self::_CREATE_USER_KEY, \Session::token());
        return \View::make('user.form', $data);
    }

    // 確認
    public function pageConfirm()
    {
        if (!\Session::get(self::_CREATE_USER_KEY)) {
            return \Redirect::to("/user/form");
        }

        $validator = \Validator::make(
            \Input::all(),
            $this->userValidateRules,
            $this->userValidateMessages
        );

        if ($validator->fails()) {
            return \Redirect::to("/user/form")
                ->withErrors($validator)->withInput();
        }
        return \View::make('user.confirm', $data);
    }
// 以下登録処理に続く
}

恐らくこのような感じで開発されているのではないかと思います。
十分綺麗!
二重送信を防ぐために、
tokenをsessionに保持して実行時に削除するという普通のフォームです。
これが大きい登録フォームですと、さすがに見にくくなってしまいますね。
とにかく小さくコンパクトに書きたい場合は、

traitを使ってみましょう という話です

もう使ってるよ!っという方も多いはずですが、使い方はシンプルです。
controllerとフォーム定義類を分けてしまいましょう。

appのどこかに適当なディレクトリを作成します
デフォルトのclassmapが良い方は、composerに下記の様に追記するといいです
app/extensions を追加しました

composer.json
"autoload": {
    "classmap": [
        "app/commands",
        "app/controllers",
        "app/models",
        "app/database/migrations",
        "app/database/seeds",
        "app/tests/TestCase.php",
        "app/extensions"
    ]
},

またはpsrでオートローダに対応させると、
dump-autoloadもいりません Laravel5からはPSRがデフォルトです

composer.json
"autoload": {
    "classmap": [
        "app/commands",
        "app/controllers",
        "app/models",
        "app/database/migrations",
        "app/database/seeds",
        "app/tests/TestCase.php",
    ],
    "psr-4": {
        "Extensions\\": "app/extensions"
    }
},

もしくはすべてオートローダに対応させます

composer.json
"autoload": {
    "classmap": [
        "app/database/migrations",
        "app/database/seeds",
    ],
    "psr-4": {
        "Application\\": "app"
    }
},
"autoload-dev": {
    "psr-4": {
        "__Test\\": "app/tests"
    }
},

*migrate類はそのままにしておくと楽かもしれません
composerに理解のある方でしたらフォルダ構成はデフォルトの状態から
大きく変えても問題ありません
フォルダ構造はお好みで変えて下さい
PSRにする場合は、ファサード等は頭に\を付けて完全修飾名にする必要があります

以降に記載するcustomValidateも追加したディレクトリで実装するとします
わかりやすくapp/extensions/Validate/Ruleを作ります。

app/extensions/Validate/Rule/User.php
namespace Extensions\Validate\Rule;

trait User
{
    // 
    public $userValidateRules = [
        'username' => 'required',
        'password' => 'required',
        'email' => 'required|email'
    ];
    // 
    public $userValidateMessages = [
        'username.required' => 'アカウント名を入力してください',
        'password.required' => 'パスワードを入力してください',
        'email.required' => 'メールアドレスを入力してください',
        'email.email' => 'メールアドレス正しく入力してください'
    ];
}

classmap利用の方は忘れずにphp composer dump-autoloadしましょう
psrの方はオートローダーが自動で解決しますのでコマンドは不要です
するとcontrollerには下記の様に書く事ができます

ExampleController.php
class UserController extends \BaseController 
{

    const _CREATE_USER_KEY = '_create_user:';

    use Extensions\Validate\Rule\User;

    // 以下同じ
}

traitはコードを再利用する仕組みですので、色んなところで使う事が出来ます
大きなフォームでもかなりコンパクトに記載出来る様になります
IoCコンテナを使って、ルールなどを注入してもかまいません
(みなさん二重送信阻止はtoken以外を使ってるのかな?)

customValidate

では続いて同じ様にcustomValidateを作成します。
先ほどのextensionsフォルダをそのまま使ってみましょう。(他のフォルダでももちろんいいですよ!)
start/global.phpに下記のものを追加します。

start/global.php
\Validator::resolver(function($translator, $data, $rules, $messages) {
        return new \Extensions\Validate\CustomValidator(
            $translator, $data, $rules, $messages);
    }
);

またはサービスプロバイダーに記述します

class ValidationExtensionServiceProvider extends ServiceProvider 
{

    public function register() {}

    public function boot() 
    {
        $this->app->validator->resolver( 
            function( $translator, $data, $rules, $messages = array()) {
                return new \Extensions\Validate\CustomValidator( 
                    $translator, $data, $rules, $messages);
            }
        );
    }

} 

たとえば、モデル(非Eloquent)などの場合、constructで下記の様に生成して
validateルールに使用する事ができます。
下記の例は、アカウント名に重複が無いか、ですが
実際にはlaravel4で用意されているもうちょっと楽なものがあります。

app/extensions/Validate/CustomValidator.php
namespace Extensions\Validate;

use Model\User;
use Illuminate\Validation\Validator;
use Symfony\Component\Translation\TranslatorInterface;

class CustomValidator extends Validator
{
    /** @var \Model\User  */
    protected $users;

    /**
     * @param User $user
     * @param TranslatorInterface $translator
     * @param type $data
     * @param type $rules
     * @param type $messages
     */
    public function __construct(
        User $user,
        TranslatorInterface $translator, 
        $data, 
        $rules, 
        $messages = []
    ) {
        parent::__construct($translator, $data, $rules, $messages);
        $this->user = $user;
    }

    /**
     * @param type $attribute
     * @param type $value
     * @param type $parameters
     * @return boolean
     */
    public function validateUniqueUser($attribute, $value, $parameters = null)
    {
        $request = $this->getData();
        if (is_null(
            $this->user->getUserAccount(
                $value,
                ($request['user_id'] != '') ? $request['user_id'] : null
            )
        )) {
            return true;
        }
        return false;
    }
}

メソッド名にprefoixとして頭にvalidateが必要になります

$valueには指定した要素の値が入りますが、
他の値を取得したい場合は、getData()などで送信された値を取得する事が出来ます

また、customValidateの利用のルールは以下の様に指定します。

customValidate
public $userValidateRules = [
    'username' => 'uniqueUser',
];

public $userValidateMessages = [
    'username.unique_user' => '入力したアカウント名は使用できません',
];

ルールにはvalidateを除いた文字列、メッセージはスネークケース
*メッセージの指定方法はこれ以外の方法もあります。

これを先ほどのtraitに追記してしまいましょう。

trait
trait User
{
    // 
    public $userValidateRules = [
        'username' => 'required|uniqueUser',
        'password' => 'required',
        'email' => 'required|email'
    ];
    // 
    public $userValidateMessages = [
        'username.required' => 'アカウント名を入力してください',
        'username.unique_user' => '入力したアカウント名は使用できません',
        'password.required' => 'パスワードを入力してください',
        'email.required' => 'メールアドレスを入力してください',
        'email.email' => 'メールアドレス正しく入力してください'
    ];
}

これでフォームのルールとcustomValidateを綺麗に分離する事が出来て、
controllerもかなり綺麗になりました。

こうしなきゃいけない!というのはありませんので、楽に開発出来るヒントになればと思います。

trait Validate
{

    public $userValidateRules = [
        'username' => 'uniqueUser',
    ];

    /**
     * @param array $attributes
     * @param string $key
     * @return bool
     */
    public function validate(array $attributes, $key)
    {
        $validate = \Validator::make($attributes, $this->rule[$key]);
        if ($validate->passes()) {
            return true;
        }

        $this->setErrors($validate->messages());
        return false;
    }

    /**
     * Set error messages
     * @param \Illuminate\Support\MessageBag
     */
    protected function setErrors($errors)
    {
        $this->errors = $errors;
    }

    /**
     * Retrieve error message bag
     */
    public function getErrors()
    {
        return $this->errors;
    }

またはこのようにして、コントローラーでもどこでも好みの場所で検証させるという方法も面白いかもしれません

明日はwebsocketですが、通常のlaravel4+websocketはcockbookに載っていますので、
サーバからのpush実装方法について書きます

*traitはphp5.4以降です