You are here
Home > Posts tagged "L5.2"

グローバル変数をダイナミックに管理

Laravelのバージョンが5になってから、グローバルスコープを持つ変数の設定は、.envconfig/のディレクトリの設定ファイルで、実用的しかも綺麗にまとまりました。変数の使用も、config('app.url')と、プログラムのでどこでも簡単に取得できます。

また、DBの設定などのデフォルト設定ファイル以外にも、独自の設定ファイルも作成できます。

例えば、

config/local.phpというファイルを作成して、


return [
        'convert' => '/usr/bin/convert', // 画像変換のプログラムのパス名
        'items_per_page' => 50,          // 1ページのアイテム数
        'max_upload_size' => 20,         // 最大アップロードの画像サイズ(MB)
        'company' => env('LOCAL_COMPANY', 'Lots of Bytes'), // 会社名
];

とすれば、config('local.company')と参照できます。

しかし、これらのグローバル変数設定の問題は、プログラムとして設定ファイルあるいは.envでハードコードしなければならないことです。

グローバル変数をDBに値を保存して、管理画面で以下のように値を編集できて、プログラムの各所で使用できるようにするには、どうしたらよいのでしょうか?

my-application

幸いにも、config()の関数は、取得だけでなく書き込みも可能なのです。


config(['global.admin_email' => 'abc@gmail.com']);

というように実行すれば、config('global.admin')abc@gmail.comを返すことができます。

global.とプリフィックスしたのは、単に他の変数と区別やグループ分けがしたいがためです。

ということは、DBからデータを読んでプログラムの実行時の最初に、configして入れしまえば良いわけです。

このための適切な場所は、プログラムの初期に実行される、app/Providers/AppServiceProvider.phpの中です。


namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use DB;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
	$setting = DB::table('setting')->pluck('value', 'key');

	config(['setting' => $setting]);        
    }
...
}

DBテーブルsettingの中身は、以下とすると、

+------------+---------------------+---------------------+------------------+-------------------+
| setting_id | created_at          | updated_at          | key              | value             |
+------------+---------------------+---------------------+------------------+-------------------+
|          1 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | email_admin      | admin@gmail.com   | 
|          2 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | email_from       | from@gmail.com    | 
|          3 | 2016-12-03 01:09:51 | 2016-12-03 01:09:51 | site_name        | ララジャパン       | 
+------------+---------------------+---------------------+------------------+-------------------+

例えば、config('setting')['site_name']の実行では、ララジャパンの値が返ってきます。

バリデーション (9) 配列のDistinctと暗黙の拡張

配列のDistinctバリデーション

前回の例題とした商品オプションに、オプション名に同じ値が入ることを禁じるバリデーションを追加します。

distinct

これには、配列プレースホルダー .* ともに laravel 5.2 で追加された distinct バリデーションが利用できます。よく考えられていますね。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*|distinct',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

 

空の入力に対するdistinct?

ところで、ここまで「オプション名」は必須入力であることを前提としてきましたが、商品オプションが1つだけなら「オプション名」は空のまま「販売単価」と「在庫数」だけ入力できるようにしたい、という要望が現場から上がってくるかもしれません。

もう少し仕様追求すると、商品オプションが2つ以上であっても、先頭(とは限らないかもしれません)の1つは空でよいかもしれません。

このために空の入力が1個まで可というバリデーションルールを追加するとロジックが複雑化します。required_with を外したとき distinct が空文字列もあわせて重複チェックしてくれれば話が簡単ですね。こんなふうに・・・

distinct_with_blank

3つの入力欄のうち2つを空にした場合のリクエストは次のようになります。

Array
(
    [option_name] => Array
        (
            [11] => 
            [12] => 
            [13] => オレンジ
        )

option_name のキー 11 と 12 は同じ値(空文字列)なのだから重複チェックにかかりそうなものです。

しかしこのバリデーションはそんな都合よく機能しません。

 

distinct バリデーションは、option_name 全体ではなく、配列の値それぞれに働きます。キー 11 の値は配列内で重複してるか? キー 12 の値は・・・と。

そして、バリデーションの原則は値が入力された項目にのみ働きます。このケースでは、そもそもキー 11 と 12 は調査されることなく、キー 13 の値が配列内で重複してるかだけが調べられ、重複なしと判定されるのです。

そう、リクエストが空でも働くのは、required 系バリデーションだけでした。

 

暗黙の拡張 implicitRules

ここまでくると次の展開が読めましたか?

この課題は解決には、distinctrequired と同様に 空リクエストに対してもバリデーションが働く 必要があり、これを実現するのが 暗黙の拡張 ルールです。

この暗黙の拡張を「暗黙の必須」と読んでしまうと理解を間違えます。空リクエストに対するバリデーション、それ以上でも以下でもありません。

暗黙の拡張ルールを作るには、Validator::extendImplicit() メソッドを使います。通常の拡張ルールとして登録されると同時に、暗黙の拡張ルールの名前を収めた配列 $implicitRules にルール名が追加されます。

Validator::extendImplicit('foo', function($attribute, $value, $parameters, $validator) {
    return $value == 'foo';
});

ということは、Validator を継承して作成した customValidaor クラスでは、直接 $implicitRules にルール名を追加することで暗黙の拡張を作ることができます。

以下は、空文字列を含んだ重複判定ができる distinct_with_blank バリデーションの作成例です。

class CustomValidator extends \Illuminate\Validation\Validator {

    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);

        // 暗黙の拡張に追加
        $this->implicitRules[] = 'DistinctWithBlank';
    }

    /**
     * 空文字列も含む重複判定 distinct_with_blank
     * @param  string $attribute
     * @param  string $value
     * @param  array  $parameters
     * @return true
     */
    public function validateDistinctWithBlank($attribute, $value, $parameters)
    {
        // 標準distinctを呼ぶだけ
        return parent::validateDistinct($attribute, $value, $parameters);
    }

これを用いて option_name.* から required_with を外すと、最初のルール定義は次のようになります。無事に、オプション名の空は1つだけ許可されるようになりました。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'distinct_with_blank',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

 

この他にも、DB上の重複をチェックする unique に対し、空入力でも機能する unique_with_blank も状況によっては(DB定義がNULL OKではないなど)ニーズがあるかもしれませんね。

バリデーション (8) 配列をバリデーションする

laravel 5.2 で、バリデーションルールに配列を表す .* というプレースホルダーが使えるようになりました。

例えば、次のような商品オプションのバリデーションを考えます。

option_name

入力行の追加UIやドラッグ&ドロップによるソートはjQueryなどで実装することにします(laravel から離れるので解説は省きます)

この場合、項目数がいくつになるかわからないので、エレメントの属性名を name="option_name[{{$id}}]" などとし、配列を返すように作りますよね。

リクエストは次のようなものになるでしょう。配列のキーは編集データに依存しますので不定です(ここでは仮に 11, 12, 13 としました)

最低必要なバリデーションとしては、「保存」が少なくとも1つチェックされることと、「保存」がチェックされた行は必須入力となること、そして「販売単価」と「在庫数」の数値判定です。

苦労していた配列のバリデーションですが、5.2 からは次のようなルール定義が可能になったのです。

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

$messages = [
  'option_id.required' => '保存するレコードを少なくとも1つ選択してください',
];

「オプション名」、「販売単価」、「在庫数」のルール定義には属性名に .* を加えることで、配列の値それぞれにバリデーションが適用されるようになります。

「保存」がチェックされた行だけ入力が必要なので、required_with の引数も option_id.* となります。一方で「保存」の必須チェックは配列全体の required です。

配列部分のエラーは array_dot() による配列ドット記法で返ってきますので、Bladeテンプレートのエラーメッセージ表示は次のような書き方になります。

{{ $errors->first("unit_price.$id") }}

 

laravel 4 や 5.1 以前での配列バリデーション

後出しジャンケンでの自慢話となってしまいますが、私たちのプロジェクトでは lavavel 4 のころから配列に対するバリデーションを、5.2 と同じルールの書き方で処理してきました。

もともと laravel のバリデーションでは、リクエストは配列のまま処理されるでのはなく、array_dot によるドット記法の一次元データに変換されて内部処理されていました。

例えば次のように、配列のキーが固定されてる入力フォームの場合ならば、

<input type="text" name="name[last]" value="{{ old('name.last') }}">
<input type="text" name="name[first]" value="{{ old('name.first') }}">
Array
(
     [name] => Array
        (
            [last] => 山田
            [first] => 太郎
        )
)

以下のようにドット記法でルールを定義することで、配列データもバリデーションできたのです。

$rules = [
  'name.last'  => 'required',
  'name.first' => 'required',
];

 

最初に、「5.2 で .* というプレースホルダーに対応した」と書き、配列バリデーションそのものに対応したと書かなかった意味がここにあります。
このプレースホルダーの変換を自分で対応すれば、5.1 以前や 4 でも同様の処理ができるわけです。

定義するルール:

$rules = [
  'option_id'     => 'required',
  'option_name.*' => 'required_with:option_id.*',
  'unit_price.*'  => 'required_with:option_id.*|integer|min:0',
  'inventory.*'   => 'required_with:option_id.*|integer|min:0',
];

変換後のルール:

$rules = [
  'option_id'      => 'required',
  'option_name.11' => 'required_with:option_id.11',
  'option_name.12' => 'required_with:option_id.12',
  'option_name.13' => 'required_with:option_id.13',
  'unit_price.11'  => 'required_with:option_id.11|integer|min:0',
  'unit_price.12'  => 'required_with:option_id.12|integer|min:0',
  'unit_price.13'  => 'required_with:option_id.13|integer|min:0',
  'inventory.11'   => 'required_with:option_id.11|integer|min:0',
  'inventory.12'   => 'required_with:option_id.12|integer|min:0',
  'inventory.13'   => 'required_with:option_id.13|integer|min:0',
];

この変換は次のような関数で対応できます。ベースコントローラかトレイトに入れて、バリデーション直前にルールの変換を挿入してみてください。

/**
 * 配列バリデーションルールの変換
 * @param \Illuminate\Http\Request $request
 * @param array                    $rules
 * @param array                    $messages
 */
public static function setArrayRules($request, &$rules, &$messages)
{
    foreach($rules as $field => $rule)
    {
        // field文字列は foo.* か?
        if (!preg_match('/^(.+)\.\\*$/', $field, $m)) continue;

        // fooはリクエストに存在して配列か?
        $name = $m[1];
        if (!($req = $request->get($name)) || !is_array($req)) continue;

        foreach(array_keys($req) as $i)
        {
            // rulesに foo.$i を複写
            $rules["$name.$i"] = str_replace('*', $i, $rule);

            // messagesにfoo.$i.barがあれば複写
            foreach($messages as $key => $message)
            {
                if (!preg_match("/^$name\.\\*\.(.+)$/", $key, $m)) continue;
                $messages["$name.$i.{$m[1]}"] = $message;
            }
        }
        unset($rules[$field]);
    }
}

 

p.s.

上で紹介した setArrayRules() では、次のような配列の記述には対応してません。もし必要とするならばご自身で考えてみてくださいね。

Bladeソース:

<input type="text" name="option[{{$id}}][name]" value="old("opton.$id.name")">

バリデーションルール:

 'option.*.name' => 'required_with:option_id.*',

バリデーション (7) エラーメッセージのリプレーサー

等号を含む日付の最大最小

日付の最大最小 after, before の比較はなぜか等号を含みません。英単語の意味を厳格にプログラム仕様に落とし込んだようですが、実際の使い勝手としては等号を含んでほしかったところです。

これを等号を含むように拡張してしまうと、前回の min, max を拡張したのと違って意味が変わってしまいます。
そこで、start, end を新たに作成して追加してみましょう。

'start_date' => 'date|start:today',      // 今日を含んでそれ以降
'end_date'   => 'date|start:start_date', // 開始日を含んでそれ以降

最低限の追加で済むように、内部で標準バリデーションの after, before を呼び出すようにします。標準が等号を含まないのですから、普通に考えると after に与えられた引数を -1(秒)し、before の引数を +1(秒)するのがわかりやすいでしょう。

しかし、これらの引数は日付そのものだけではなく、上に示したように比較対象の属性名が与えられることがありますから、加減算をするには属性名から参照先の日付を取得するコードを自分で書かなければなりません。

そこで発想を逆転して、検査する入力値の日付を +1(秒)して after に、-1(秒)して before に与えることにします。意味を考えると分かりづらいですが、相対的なものなので正負を逆転しただけです。

作成したバリデーションルールは次のとおりです。

public function validateStart($attribute, $value, $parameters)
{
    $value = date('Y-m-d H:i:s', strtotime($value.' +1 sec'));

    return parent::validateAfter($attribute, $value, $parameters);
}

public function validateEnd($attribute, $value, $parameters)
{
    $value = date('Y-m-d H:i:s', strtotime($value.' -1 sec'));
/*
    // 比較相手が 日付のみ場合は 23:59:59を補完する
    if ((($date = $this->getValue($parameters[0])) || ($date = $parameters[0]))
        && !preg_match('/:/', $date))
    {
        $parameters[0] = date('Y-m-d 23:59:59', strtotime($date));
    }
*/
    return parent::validateBefore($attribute, $value, $parameters);
}

validateEnd() のコメントされたコード部分は、DatetimeDate を比較したときに起きる問題の補正です。

例えば、DBレコードの販売期間が Datetime で、出荷期間が Date のときなど、異なる日付型の間で比較が必要な場合には有効にしてください。
補正は end のみに必要で、start には必要ありません。

$tests = [
    ['2016-10-10', '2016-10-10 10:00:00'],
    ['2016-10-10 10:00:00', '2016-10-10'],
];
foreach($tests as $i => $date) {
    printf("%d => %d %d\n", 
    $i, ($date[0] >= $date[1]), ($date[0] <= $date[1]));
}

結果

0 => 0 1 // ('2016-10-10' >=' 2016-10-10 10:00:00') == FALSE は正しい!
1 => 1 0 // ('2016-10-10 10:00:00' <= '2016-10-10 23:59:59'に補正が必要

 

メッセージへの引数の展開

さて、前回、比較対象の属性名を引数に持てるように拡張した min, max において、エラーメッセージの引数部分が、バリデーション言語ファイルの定義通りの日本語に展開されませんでした。
今回追加作成した start, end にいたっては、英文字の属性名にすら展開されません。この問題を解決しましょう。

今、バリデーション言語ファイルが次のように定義されているとします。

resources/lang/ja/validation.php

'after'  => ':attributeが:dateより前',
'before' => ':attributeが:dateより後',
'max' => [
  'numeric' => ':attributeが:maxよりも大きい',
],
'min' => [
  'numeric' => ':attributeが:minよりも小さい',
], 

// 追加

'start'  => ':attributeが:dateより前',
'end'    => ':attributeが:dateより後',

// 項目名

'attibutes' => [
  'end_date'   => '終了日',
  'start_date' => '開始日',
  'unit_cost'  => '仕入単価',
  'unit_price' => '販売単価',
],

このとき、それぞれのルールに対するエラーメッセージは次のように展開されます。

区分 ルール エラーメッセージ 課題
標準 ‘unit_price’ => ‘min:0’ 販売単価が0より小さい
拡張 ‘unit_cost’ => ‘max:unit_price’ 仕入単価がunit_priceより大きい 日本語にはならない
標準 ‘end_date’ => ‘before:start_date’ 終了日が開始日より後
追加 ‘end_date’ => ‘end:start_date’ 終了日が:dateより後 属性名への展開すらない

属性名を引数に取れるように拡張した min, max で英文字の属性名が展開できるのは、数値が与えられたときの標準の展開がそのまま適用されているためです。
 

カスタムプレースホルダー

エラーメッセージの中の :attribute:min, :date はプレースホルダーと呼ばれます。

カスタムのプレースホルダーを追加するには、サービルプロバイダーの boot メソッドの中で、Validator::replacer() メソッドを呼び出します。

Validator::replacer('foo', function($message, $attribute, $rule, $parameters) {
    return str_replace(...);
});

もしくは、Validator を継承した CustomVlidator クラスでは、addReplacer() メソッドか、複数のリプレーサーを配列にしてまとめて登録できる addReplacers() メソッドで登録できます。

$this->addReplacer('foo', function($message, $attribute, $rule, $parameters) {
    return str_replace(...);
});

リプレーサーの中身は簡単な文字列置換です。

与えられたエラーメッセージ($message)に対して、プレースホルダー文字列(":min"":date" など)を引数の文字列($parameters[0])に置き換えるコードを記述します。

引数($parameters[0])は与えられた最小値の値や、比較相手の英文字の属性名です。これを日本語に展開するには、バリデーション言語ファイルの $attributes 配列から対応語を参照する getAttribute() メソッドを使います。

 

以下、CustomValidator クラスのコンストラクタで min, max, start, end にリプレーサーを登録するサンプルです。共通部分が多いのでクロージャを変数に代入し、setReplacers() でまとめて配列登録しました。

これで、エラーメッセージは期待通りすべて日本語に展開されるようになりました。

<?php

namespace App\Services;

class CustomValidator extends \Illuminate\Validation\Validator
{
    public function __construct($translator, $data, $rules, $messages = [])
    {
        parent::__construct($translator, $data, $rules, $messages);

        // 汎用(ルールと同名)
        $replacer = function($message, $attribute, $rule, $parameters) {
            return str_replace(':'.$rule, $this->getAttribute($parameters[0]), $message);
        };
        // 日付
        $date = function($message, $attribute, $rule, $parameters) {
            return str_replace(':date', $this->getAttribute($parameters[0]), $message);
        };

        $this->addReplacers([
            'min' => $replacer, 'max' => $replacer, 'start' => $date, 'end' => $date
        ]);
    }

バリデーション (6) 他の属性の値を参照

日付の最大最小 after, before では、引数に値(日付)を与えるだけでなく、比較対象の属性の名前を与えることができます。
むしろ具体的な日付を引数にすることのほうがまれでしょう。

'date_start' => 'date|before:today',
'date_end'   => 'date|after:date_start',

このような使い方は、数値型の最大最小 min, max でも使用したいこともありますよね?

例えば、次のような関係です。

unit_cost

'unit_price' => 'required|integer|min:0',
'unit_cost'  => 'required|integer|min:0|max:unit_price',

標準バリデーションの minmax を拡張して、比較対象の属性名を引数に持てるようにしてみましょう。

 

getValue()

バリデーションクラスにルールを登録するときの雛形は次のようなものでした。
一見すると引数には、ルールを定義した属性に関する情報しか与えられません。

/**
 * 追加バリデーション
 * @param  string  $attribute  検査する属性名
 * @param  string  $value      入力された値
 * @param  array   $parameters 引数の配列
 * @return boolean
 */
public function validateHoge($attribute, $value, $parameters)
{
  //
}

しかし、バリデーションクラスにはリクエストされたすべての属性に関する情報が与えれていて、他の属性に入力された値も getValue() で得ることができます。getValue() の引数は属性名です。

よって、次のバリデーションは必ず TRUE を返します。

public function validateHoge($attribute, $value, $parameters)
{
   return ($value == $this->getValue($attribute));
}

 

min と max の拡張

min, max の引数は、属性の型が何であろうと必ず数値を取ります。

そこで、引数が数値(is_numeric)でないときを限定し、引数を属性名とした値が存在するならそれを新たなしきい値としました。

/**
 * 最小値 min
 * @param string $attribute
 * @param string $value
 * @param array $parameters	0 => 比較する属性名
 * @return true
 */
protected function validateMin($attribute, $value, $parameters)
{
    if (!is_numeric($parameters[0]) && 
        !is_null($val = $this->getValue($parameters[0])))
    {
        $parameters[0] = $val;
    }
 
    return parent::validateMin($attribute, $value, $parameters);
}

/**
 * 最大値 max
 * @param string $attribute
 * @param string $value
 * @param array $parameters	0 => 比較する属性名
 * @return true
 */
protected function validateMax($attribute, $value, $parameters)
{
    if (!is_numeric($parameters[0]) &&
        !is_null($val = $this->getValue($parameters[0])))
    {
        $parameters[0] = $val;
    }
 
    return parent::validateMax($attribute, $value, $parameters);
}

 
* * * *

さて、拡張した最大最小バリデーションですが、このままではエラーメッセージが与えられた引数の属性名のままとなり、言語ファイルによる置き換えが行われません。

unit_cost2

本来の最大最小が与えられた引数そのものを、メッセージのプレースホルダー :max と置き換えることしか想定していないからです。
この部分の解決については次回にまとめる予定です。

バリデーション (5) 最大値と最小値

バリデーション言語ファイルを編集するときにお気づきと思いますが、Laravel標準バリデーション MaxMin は(他に SizeBetween も)調査対象の属性の型でその挙動を変えます。

'max' => [
    'numeric' => ':attributeの値が:maxを超えています',
    'file'    => ':attributeのサイズが:max kBを超えています',
    'string'  => ':attributeの文字数が:maxを超えています',
    'array'   => ':attributeの個数が:maxを超えています',
],
'min' => [
    'numeric' => ':attributeの値が:minに足りません',
    'file'    => ':attributeのサイズが:min kBに足りません',
    'string'  => ':attributeの文字数が:minに足りません',
    'array'   => ':attributeの個数が:minに足りません',
],

ここで問題となるのは、文字列型が入力した文字数カウントとの比較になることです。文字列比較の大小によるバリデーションは存在してません。

例えば日付型の属性に対する開始日のつもりで Min を指定しても、文字数判定となり、期待したバリデーションにはなりません。

'date_start' => 'date|min:'.date('Y-m-d'),    // 開始日判定にはならない

 

日付の最大値と最小値

日付型の属性の前後関係の判定には、専用の afterbefore が用意されています。
しかも、引数の文字列は strtotime で処理されるので次のような指定が可能です。

'date_start' => 'date|after:today',    // 今日より大(明日以降)
'date_end'   => 'date|befor:+1 year',  // 1年以内

または属性名を引数に持つことができるので、期間指定のバリデーションに使うことが出来ます。

'date_end'   => 'datetime|after:date_start',

invalid_period

ただし、after, befor の比較は等号を含みませんので注意が必要です。

 

数値型のカスタムバリデーション

例えば1以上の整数(自然数)を判定するカスタムバリデーション natural_number を作成した場合、それを minmax で大小判定したときは、正しく数値として判定されるでしょうか?

結論を言えば、カスタムバリデーション natural_number は(そのままでは)、入力された値が数値であっても、文字列として判断され、文字数でカウントされてしまいます。

'quantity' => 'requied|natural_number|max:10', // 10桁( 10億)まで可

 

では、数値として認められるにはどのようにしたら良いのでしょうか。

vender の Validation.php でコードを追うと、getSize() メソッドにおいて次のように型判定されていることがわかります。

1 入力値が数値(is_numeric)でバリデーションに numericinteger があれば、その値を返す
2 入力値が配列(is_array)なら配列の個数を返す
3 入力値が File インスタンスならファイルサイズを kB で返す
4 それ以外は文字数(mb_strlen)を返す

数値として判定されるには、入力された値が数値であるかどうかだけでなく、バリデーションに numericinteger が含まれなければなりません。これでは他のどんなルールであっても数値と判定されることはないのです。

 

numericRules

この数値と判断されるためのバリデーションルールは、変数 $numericRules で定義されていました。

protected $numericRules = ['Numeric', 'Integer'];

カスタムバリデーションとして数値型のルールを自作した場合には、これにルール名を追加すればよいわけです。

変数 $numericRulesprotected で再定義して上書きするか、コンストラクタで必要分だけ追加します。……再定義する場合は NumericInteger を忘れずに。

public function __construct($translator, $data, $rules, $messages = [])
{
    parent::__construct($translator, $data, $rules, $messages);

    $this->numericRules[] = 'NatrualNumber';
}

/**
 * 自然数 natural_number
 *
 * @param string $attribute		検査する属性名
 * @param string $value			検査する値
 * @return bool
 */ 
public function validateNaturalNumber($attribute, $value)
{
    if ($this->validateInteger($attribute, $value))
    {
        return ($value > 0);
    }

    return false;
}

バリデーション (4) DateとDatetime

Laravel 標準の Date バリデーションは、次のときに TRUE を返します。

OR 値がPHPの DateTime クラスのインスタンスである
AND 値が strtotime で理解できる文字列である
date_parse で年月日を返す
checkdate で年月日が妥当である

文字列の具体例でまとめると以下のようになります。

文字列 バリデーション MySQL保存
例01 now FALSE
例02 +1 day FALSE
例03 next Thursday FALSE
例04 2016-09-30 + 1day TRUE ×
例05 30 September 2016 TRUE ×
例06 2016-09-30 TRUE
例07 20160930 TRUE
例08 2016/09/30 TRUE
例09 09/30/2016 TRUE ×
例10 09-30-2016 FALSE
例11 30/09/2016 FALSE
例12 2016-09-30 10:10:00 TRUE

date_parse を通すため 例01〜例03のような論理指定は弾かれますが、例04のような記述では前半部分が年月日の配列値を返すためバリデーションは通ります。
また、アメリカ式の月日年(例09)が通って、ヨーロッパ式の日月年(例11)は弾かれます(これは環境のタイムゾーンに影響されるでしょう)。

いずれにせよ、バリデーションに通ってもそのままデータベースに保存できるとは限りません。
データベース保存を前提にするなら、ISO-8601 の "YYYY-MM-DD" 書式に限定したほうが何かと都合が良いですね。

 

date_format による ISO-8601限定

入力され日付のフォーマットを限定するために date_format が用意されています。引数は date_parse_from_format を通して処理されます。
datedate_format は両方を同時に使用することはできません。

'date'     => 'date_format:Y-m-d',
'datetime' => 'date_format:Y-m-d H:i:s',

 

Date と Datetime の拡張

データベースの保存フィールドで DateDatetime を区別し、入力フォームのインターフェースもこの2つしかないならば、date_format を使用せずに、date バリデーションを拡張してしまうこともできます。

書式を正規表現で限定してからオリジナルのバリデーションを通せばよいでしょう。

    /**
     * date (without datetime)
     *
     * @param string $attribute
     * @param string $value
     * @return true
     */
    protected function validateDate($attribute, $value)
    {
        // YYYY-MM-DD に限定する
        if (!preg_match('/^[0-9]+-[0-9]+-[0-9]+$/', $value)) return false;

        return parent::validateDate($attribute, $value);
    }

    /**
     * datetime
     *
     * @param string $attribute
     * @param string $value
     * @return bool
     */
    public function validateDatetime($attribute, $value)
    {
        // YYYY-MM-DD hh:mm に限定する
        if (!preg_match('/^[0-9]+-[0-9]+-[0-9]+ ([0-9]+):([0-9]+)/', $value, $m)) return false;

        return parent::validateDate($attribute, $value);
    }

validateDete() は標準の置き換えなので protected 宣言ですが validateDatetime() は追加関数として public で宣言しています。

 

日付入力のフォームインターフェイス

入力バリデーションとして DateDatetime を用意したならば、入力フォームもそれに対応しなければなりませんが、HTML5 の日付フォームはブラウザの対応に依存し、非対応な環境を無視したとしてもユーザーインターフェイスが人によって異なりサポートが難しいものです。

私たちのプロジェクトの管理画面では、Bootstrapベースの SmartAdmin を導入してインターフェイスの統一を図っています。

SmartAdmin の日付フォームは jQuery UI DatepickerBootstrap Timepicker の組み合わせを採用していますが、HTML5の datetimedatetime-local には対応してません。スクリプトを書くことで datetime 属性(厳密には datetime-local 属性)の入力フォームを実現する必要があります。

datetime

bladeソース(の概念)と対応するjQueryスクリプト

<div class="input-group-datetime">
  <input type="hidden" name="date_start" value="{{ old('date_start') }}">
  <input class="datepicker">
  <input class="timepicker">
</div>
$('.input-group-datetime').on('change', 'input:visible', function() {
  // 日付または時刻が変更された
  var $parent = $(this).parents('.input-group-datetime'),
    dt = $parent.find('.datepicker').val(),
    tm = $parent.find('.timepicker').val();
  if (dt && !tm) {
    // 時刻が空なら00:00で補完
    tm = '00:00';
    $parent.find('.timepicker').val(tm);
  }
  if (!dt) {
    // 日付が空なら時刻も削除
    $parent.find('.timepicker').val('');
  }
  // 日時を合成して値を格納
  $parent.find(':hidden').val((dt) ? dt + ' ' + tm + ':00' : '');
});

ちょっとしたパフォーマンスの改善

Laravelは非常にたくさんのファイルを起動時に読み込んでいるので、パフォーマンスの改善は以前から感心があります。最近、管理画面だけでrouteの数が300近いプログラムをインストールするにあたり、重たくなることを予想して、簡単にできる範囲でLaravelでのパフォーマンスの改善を調査してみました。

1.設定ファイルのキャッシュ作成

php artisan config:cache

この実行は、configのディレクトリにあるファイルすべてを合わせて1つのキャッシュファイルにします。これにより設定データのローディング時間を速めようということです。

ファイルは、

bootstrap/cache/config.php

として作成されます。

この使用において、注意は3点あります。

注意点1:プログラムにおいてenv()の関数を使用してはいけない

例えば、

.envのファイルにおいて、

APP_ENV=local

と設定していて、

$path = storage_path().'/'.env('APP_ENV').'/logs/test.log';

のように、現在の環境変数の値によりログファイルを置くディレクトリを変えるとすると、

$pathの値は、

/var/www/test/storage/local/logs/test.log

のようになります。

しかし、php artisan config:cacheを実行すると、env()の値は空となり、

/var/www/test/storage/logs/test.log

となってしまい、意図した場所とは違うことになってしまいます。

それゆえに、先のプログラムは以下と変更することが必要です。

$path = storage_path().'/'.config('app.env').'/logs/test.log';

もちろん、config/app.phpなどの設定ファイルの中でのenv()の使用は問題ないです。

注意点2:ダイナミックに値を設定しているconfigに気をつける

設定ファイルの値は、

config(['some_setting' => 'some_value']);

のようにダイナミックに値の設定が可能です。例えば、DBからの値をAppServiceProvider::boot()で読み込んでおいて、DBに再度アクセスすることなくプログラムのあちらこちらで使用しようというときなどに便利です。

ところが、php artisan config:cacheの実行時に、AppServiceProvider::boot()が実行されるので、それらの設定もキャッシュファイルに保存されてしまいます。これで、ダイナミックに変わる値が固定されては困ります。

しかし、ダイナミックに上書きされるので問題はなさそうです。しかし、プログラムの他の部分では問題があるかもしれません。使用には気をつけてください。

注意点3:.envやconfigのファイルを編集したら、必ずキャッシュを再作成

最後に忘れてはならないのは、.envconfig/*.phpのファイルを編集したら、必ずphp artisan config:cacheの実行が必要なことです。それなくしては、せっかくの変更も反映されません。

2.routeのキャッシュ作成

php artisan route:cache

こちらは、config:cacheと違い、この実行で作成されるファイル、

bootstrap/cache/routes.php

には、routes.phpファイルを読み込んでLaravelがマップしたデータ構造をbase64_encodeして、serializeした形で収めます。それゆえに、この読み込みとマップの作業を一気に短縮します。とくに、routeの数が大きいプロジェクトではパフォーマンスの改善に期待できる仕組みです。

それからconfig:cacheと同様に、routes.phpファイルを編集したら、必ずphp artisan route:cacheの実行が必要です。

3.共有クラスファイルの最適化

これは、環境変数のAPP_ENVがproductionの値のときにだけに有効なものです。また、設定ファイルに依存するので、設定ファイルをキャッシュするなら、php artisan config:cacheを実行してから以下を実行してください。

php artisan optimize

この実行で、共有されるクラスファイルを1つのファイルにまとめ、読み込み時間を短縮するのが目的です。以下のファイルを作成します。

bootstrap/cache/compiled.php

このファイルには、デフォルトでは、Laravel関連のファイルが含まれますが、必要なら以下の設定ファイルに追加が可能です。

bootstrap/cache/compile.php

改善した?

さて、実際にこれらの最適化でパフォーマンスはどれくらい変わるのでしょうか?

私の厳密ではないテストでは、DB操作を伴わない計測では10~30%の違いがありました。configよりもrouteやoptimizeの方が実際のパフォーマンスにより影響ありました。多分、設定ファイルが小さく数少ないためがその違いと思います。しかし、現実では、DBクエリーやCSS、JS、画像などのダウンロードがはるかに時間がかかるので、まだ私のプロジェクトのスケールではちょっとした改善というところです。スケールがより大きくなるとかなりの差となるかもしれません。

入力のブラックリストとホワイトリスト

このブログを開始してから、もうすでに1年以上。RawのSQLを書いてコードに埋め込む日常から、Eloquentを使用したORMのコードへと日常へと移行しています。Eloquentに関しても、ブログを書き始めた頃からは理解が深まり、洗練されたLaravelのコードを書けるようになってきたこの頃です。

1年前に書いた「マスアサインメントで一括取り込み」のトピックで、EloquentのModelのクラスの属性fillableguardedの話、1年の経験で学んだことを含めてここでもう一度説明します。

まず、話のお膳立てを。

DBテーブルmemberにおいて以下の項目があるとします。

+----------------+------------------+------+-----+---------------------+----------------+
| Field          | Type             | Null | Key | Default             | Extra          |
+----------------+------------------+------+-----+---------------------+----------------+
| member_id      | int(10) unsigned | NO   | PRI | NULL                | auto_increment |
| active_flag    | char(1)          | NO   |     | NULL                |                |
| name           | varchar(255)     | NO   |     | NULL                |                |
| email          | varchar(255)     | NO   | UNI | NULL                |                |
| password       | varchar(60)      | NO   |     | NULL                |                |
| memo           | varchar(100)     | YES  |     | NULL                |                |
| created_at     | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
| updated_at     | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
+----------------+------------------+------+-----+---------------------+----------------+

MemberのModelは以下のような定義になります。

...
class Member extends Model
{
    protected $table = 'member';
    protected $primaryKey = 'member_id';
    protected $fillable = ['name', 'email'];
    ...
}

$timestamps$incrementingの定義は必要ないです。両方ともデフォルトでtrueなので。

入力フォームは、

Laravel 2016-08-31 20-51-04

で、email, password, nameの項目を入力できます。

以下のコントローラで、入力フォームから入ってきた値は以下のコードでDBに保存できます。

...
class MemberController extends Controller
{
    ...
    public store(Request $request)
    {
        $member = new Member;
        $member->fill($request->all())->save();
        ...
    }
    ...
}

しかし、先ほどの$fillable定義により、DBに保存されるのは、emailとnameのみです。実行されるSQLは以下で、他の入力された値は無視されるので、passwordはDBはデフォルトの空のままです。つまり、$fillableは、DBに入力したい値をリストするホワイトリストです。ちなみに、created_at, updated_atの項目は、Eloquentにより自動的に保存時の日時を記録します。

逆に、DBに入れたくない項目をリストするなら、つまりブラックリストを定義したいなら、$fillableの代わりに、$guardedを使用します。

...
class Member extends Model
{
    protected $table = 'member';
    protected $primaryKey = 'member_id';
    protected $guarded = ['member_id', 'active_flag', 'password'];
    ...
}

以上がマスアサイメントの使用の仕方で、意図的あるいは間違って入力フォームから、DBへ保存されるのを防いでくれます。

$fillable$guardedの目的を理解したところで、2つ問題。

まず、

active_flagやpasswordなどマスアサインメントで相手にしない項目の値はどうやってDBに保存するのでしょう?

これは、通常のオブジェクトの値のアサインメントで行います。

...
    public store(Request $request)
    {
        $member = new Member;
        $member->active_flag = 'Y'; //デフォルト
        $member->password = bcrypt($request->password); //ハッシュ値に変換
        $member->fill($request->all())->save();
        ...
    }
...

次に、
入力画面によりDBに入れたい項目が違う場合は、どう$fillableを設定?

例えば、管理画面で会員の情報を編集する画面。そこでは、会員が有効か無効のフラッグ(active_flag)、そして会員に関するノート(memo)も付加情報としてDBに保存したいです。もちろん、裏で使用するのは、同じMemberのクラスなので、同じ$fillableは使えないですね。

1つは、先の値のアサイメント使用する方法。

    $member->active_flag = $request->active_flag;
    $member->memo = $request->memo;
    $member->fill($request->all())->save();

しかし、これではもっと項目が増えたら面倒です。

$fillableを使用するのではなく、先のように$guardedをMemberで定義して、以下のようにコントローラにおいて、独自の$fillableを使用します。

...
    public store(Request $request)
    {
     $fillable = ['email', 'name'];
        $input = array_only($request->all(), $fillable);
        $member = new Member;
        $member->active_flag = 'Y';
        $member->password = Hash::make($request->password);
        $member->fill($input)->save();
        ...
    }

    public edit(Member $member, Request $request)
    {
        $fillable = ['email', 'name', 'active_flag', 'memo'];
        $input = array_only($request->all(), $fillable);
        $member->fill($input)->save();
        ...
    }
...

array_onlyの関数は、Laravelのヘルパー関数です。

routesを使いこなす(5)モデルとのバインディング

ユーザーがアクセスするURLを理解して、必要な関数にマップするのがroutes.phpの基本的な仕事です。

それらのURLには、以下のようにいろいろな形があります。


http://localhost/admin/login
http://localhost/admin/product/156
http://localhost/admin/product/156/edit

さて、上の例の156の数字は、DBテーブルのproductの主キーの値なのですが、Laravelはこの値をどのようにコントローラに取り込むのでしょう?

まず、前回のroutes.phpの設定を見てみましょう。

Route::resource('product', 'ProductController');

は、

php artisan route:list

の出力では以下のようなマップになります。

+--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+
| Domain | Method    | URI                                | Name                   | Action                                                                | Middleware      |
+--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+
|        | POST      | admin/product                      | admin.product.store    | App\Http\Controllers\Admin\ProductController@store                    | web             |
|        | GET|HEAD  | admin/product                      | admin.product.index    | App\Http\Controllers\Admin\ProductController@index                    | web             |
|        | GET|HEAD  | admin/product/create               | admin.product.create   | App\Http\Controllers\Admin\ProductController@create                   | web             |
|        | GET|HEAD  | admin/product/{product}            | admin.product.show     | App\Http\Controllers\Admin\ProductController@show                     | web             |
|        | DELETE    | admin/product/{product}            | admin.product.destroy  | App\Http\Controllers\Admin\ProductController@destroy                  | web             |
|        | PUT|PATCH | admin/product/{product}            | admin.product.update   | App\Http\Controllers\Admin\ProductController@update                   | web             |
|        | GET|HEAD  | admin/product/{product}/edit       | admin.product.edit     | App\Http\Controllers\Admin\ProductController@edit                     | web             |
+--------+-----------+------------------------------------+------------------------+-----------------------------------------------------------------------+-----------------+

URIの部分を見てください。

例えば、7行目の

admin/product/{product}

は、

ProductController@show

にマップされています。

そして156のIDは、この{product}の部分に対応します。

admin/product/{product}/edit

も同じことです。

対応するコントローラのメソッドを見ると、

namespace App\Http\Controllers;
 
use Illuminate\Http\Request;
use App\Http\Requests;
use App\Models\Product;

class ProductController extends Controller
{
    /**
     * 表示画面
     *
     * @param  \App\Models\Product $product
     * @return \Illuminate\Http\Response
     */
    public function show(Product $product)
    {
        return $product->name; //画面に商品名を表示
    }
...

showのパラメータが、数字でなくモデルのProductのタイプになっていますね。
これは、Laravelが自動的に、IDの数字をもとに、

$product = Product::find(156);

のようなモデルバインディング(紐づけ)操作を行って、Eloquentのオブジェクトを生成して、メソッドの中で使用できるようにしてくれているのです。便利ですね。

さて、product_id = 156に対応するDBレコードがない場合はどうなるのでしょう?

Sorry, the page you are looking for could not be found.
2/2 NotFoundHttpException in Handler.php line 102: No query results for model [App\Product]. 

1/2 ModelNotFoundException in Builder.php line 290: No query results for model [App\Product]. 

「検索結果が空」の404エラーとなります。

今度は、上のshowの関数のパラメータ名を以下のように変えたとしたら、どうなるのでしょう?

    public function show(Product $a_product)
    {
        return $a_product_name;//画面は空
    }

この場合、$a_project_nameにはDBレコードが入らず、単にProjectの新規オブジェクトとなり、画面には何も表示されません。

つまり、パラメータ名は、{product}とまったく同じ名前である必要があるということです。ミススペルに気をつけましょう。

さて、今度は、product_idではなく、例えば、skuという商品番号の項目の値でレコードを引っ張ってきたいときはどうするのでしょう? 

つまり、

http://localhost/admin/product/ABCDE

でアクセスしたい。そこでは、ABCDがskuの値とします。

その場合は、Productのモデルの定義で、

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $table = 'product';
    protected $primaryKey = 'product_id';

    public function getRouteKeyName()
    {
        return 'sku';
    }
  ...
}

のように、getRouteKeyNameの関数を作成して、skuをリターンすれば、product_idの代わりに以下のようにオブジェクトを作成してくれます。


$product = Product::where('sku', 'ABCD')->first();

もちろん、skuは、productのDBテーブルで、主キーと同様に1つのレコードを特定するためにユニークなキーを持つ必要あります。

最後に、

http://localhost/admin/product/156?print=Y

の場合、printの値はどう取ってくるのでしょう?

これはshowメソッドを以下のように変更して、

...
    /**
     * 表示画面
     *
     * @param  \App\Models\Product $product
     * @param  \Illuminate\Http\Request
     * @return \Illuminate\Http\Response
     */
    public function show(Product $product, Request $request)
    {
        $print = $request->input('print');

        return $print;
    }
...

printの値の取得が可能です。ここ、関数のパラメータの順番はURLでの順番とは関係ありません。逆でも同じ結果となります。先に説明したように、{product}の名前と一致する変数名でLaravelは判断するからです。

Top