FuelPHPで別フィールドを参照するバリデーションルール

はじめに

このエントリーは、FuelPHP Advent Calendar 2015の11日目です。

FuelPHPのValidationクラスは、用意されたバリデーションルール以外にも独自のルールを簡単に追加することができます。

今までいくつも作ったことがありますが、今回ちょっとだけハマったのでコードを読んで理解した内容を残しておきます。

バージョン

FuelPHP 1.7.2

追加したいルール

2つのフィールドのうち、どちらか一方だけ必須入力としたい。*1
requred_with(ある項目が入力ありなら必須)の変形みたいな感じ。

他にも、選択肢の中からN個以上必須とか、ある項目の入力内容で次の項目のバリデーションが異なるとか、検査のために別のフィールドの内容が必要になるパターン全般に共通です。

うまくいかなかったバージョン

バリデーションメソッド

<?php
public static function _validation_required_which($val, $field)
{
  //現在アクティブなValidationオブジェクトのinput配列を取得
  $input = Validation::active()->input();

  if (!array_key_exists($field , $input)) {
    return false; //ここに引っかかった
  }

  //両方からっぽ
  if (Validation::_empty($input[$field]) and Validation::_empty($val)) {
    return false;
  }

  //両方指定あり
  if (!Validation::_empty($input[$field]) and !Validation::_empty($val)) {
    return false;
  }

  return true;
}

呼び出し側(抜粋)

<?php
$valid = Validation::forge();

$valid->add_callable('Util_Validations');
$valid->add('hoge', 'HOGE')
->add_rule('required_which', 'fuga');
$valid->add('fuga', 'FUGA')
->add_rule('required_which', 'hoge');

if (!$valid->run()) {
  $error = array();

  foreach ($valid->error() as $key => $e) {
    $error[$key] = array(
        'message' => $e->get_message(),
        'type'    => $e->rule
        );
  }

  Session::set('error', $error);
  return Response::redirect('/input');
}

結果

一つめのフィールドである hogeを検査した際に、 if (!array_key_exists($field , $input)) return false; に引っかかってしまいました。

つまり、比較対象である fuga が入力フォームの配列になかったということらしい。

念のため Input::post を確認したところ問題なく取得。

原因

結論としては Validation::run() の仕様を誤解していたためでした。

すべてのフィールドを追加したら、バリデーションを実行することが出来ます。 デフォルトでは $_POST ですが、入力を与えることで、上書きや拡張することが出来ます。 Validation - クラス - FuelPHP ドキュメント

これを読んで、引数無しで Validation::run() を実行すると $_POST、つまり Input::post を渡したのと同等になると理解したのですが、実際には微妙に違っていて、検査するフィールドごとにInput::postから取得していました。

自作メソッドでは Validation::input() を使って input 配列を取得しているのですが、一つめのフィールド hoge の検査時点では hoge 自身しか入っていませんでした。

コードを読んでみる

Validation::run() (抜粋)

<?php
public function run($input = null, $allow_partial = false, $temp_callables = array())
{
  //省略

  $this->validated = array();
  $this->errors = array();
  $this->input = $input ?: array(); // ここが array() ではなく、Input::post だと思ってた
  $fields = $this->field(null, true);
  foreach($fields as $field) // add_field()やadd() したフィールドを順に処理
  {
    static::set_active_field($field);

    // convert form field array's to Fuel dotted notation
    $name = str_replace(array('[', ']'), array('.', ''), $field->name);

    $value = $this->input($name); // input配列から値を取得、配列内になかったらここで追加
    if (($allow_partial === true and $value === null)
        or (is_array($allow_partial) and ! in_array($field->name, $allow_partial)))
    {
      continue;
    }
    try
    {
      foreach ($field->rules as $rule)
      {
        $callback  = $rule[0];
        $params    = $rule[1];
        $this->_run_rule($callback, $value, $params, $field);
      }
      if (strpos($name, '.') !== false)
      {
        \Arr::set($this->validated, $name, $value);
      }
      else
      {
        $this->validated[$name] = $value;
      }
    }
    catch (Validation_Error $v)
    {
      $this->errors[$field->name] = $v;

      if($field->fieldset())
      {
        $field->fieldset()->Validation()->add_error($field->name, $v);
      }
    }
  }

  //省略
  return empty($this->errors);
}

Validation::input()

<?php
public function input($key = null, $default = null)
{
  if ($key === null)
  {
    return $this->input; // 引数なしだと現在のinput配列を返す
  }

  // key transformation from form array to dot notation
  if (strpos($key, '[') !== false)
  {
    $key = str_replace(array('[', ']'), array('.', ''), $key);
  }

  // if we don't have this key
  if ( ! array_key_exists($key, $this->input))
  {
    // it might be in dot-notation
    if (strpos($key, '.') !== false)
    {
      // check the input first
      if (($result = \Arr::get($this->input, $key, null)) !== null)
      {
        $this->input[$key] = $result;
      }
      else
      {
        $this->input[$key] =  $this->global_input_fallback ? \Arr::get(\Input::param(), $key, $default) : $default;
      }
    }
    else
    {
      // do a fallback to global input if needed, or use the provided default
      $this->input[$key] =  $this->global_input_fallback ? \Input::param($key, $default) : $default; //ここでInput::paramから取得
    }
  }

  return $this->input[$key];
}

global_input_fallbackというのは、Validationクラスの設定で、デフォルトtrue設定。
なので、input配列に存在しなかったらInput::paramから探します。*2

true にした場合で、入力された配列に値が見つからなかった場合、値は Input::param になります。 Validation - クラス - FuelPHP ドキュメント

呼び出し時にバリデーション対象を探して内部の配列に追加というのは、個人的にはちょっと行儀がよくない動きのように感じましたがどうなんでしょう。
使わない(ルールを適用しない)フィールドがたくさんある場合は効率いいのかな。

解決策その1

run(Input::post()) のように、引数に明示的にPOSTデータを渡してやれば実行時に全て保持しているので失敗しません。

でもルールによって呼び出し側で考慮しないといけないので、イマイチな気がします。

解決策その2

バリデーションメソッドを改良する。

<?php
public static function _validation_required_which($val, $field)
{
  $valid = Validation::active();

  if ($valid->_empty($valid->input($field)) and $valid->_empty($val)) {
    return false;
  }

  if (!$valid->_empty($valid->input($field)) and !$valid->_empty($val)) {
    return false;
  }

  return true;
}

Validation::active()->input(); としていたのを、Validation::active(); としてアクティブなインスタンスを取得して全てインスタンス経由で操作するように変更。

Validation::input() を引数指定で呼び出すことにより指定フィールドが input 配列になかった場合に Input::post を探しに行くようになり、期待どおりの動作が実現できました。

おまけ

Validationクラスで扱えるのは連想配列ということになっている*3のですが、今回の流れの途中でコードを読んでみたらちょっと違いました。

以下のサンプルの1つ目は動きますが、2つ目は add() した時に例外が発生します。

<?php
$valid = \Validation::forge();
$valid->add(1, 'hoge')->add_rule('required');
$valid->run(array('a', 'b'));
<?php
$valid = \Validation::forge();
$valid->add(0, '1st element')->add_rule('required');
$valid->run(array('a', 'b'));

これは、Validation::add() の実体である Fieldset::add()empty() を使ってチェックしているから。

<?php
public function add($name, $label = '', array $attributes = array(), array $rules = array())
{
  //省略

  if (empty($name) || (is_array($name) and empty($name['name'])))
  {
    throw new \InvalidArgumentException('Cannot create field without name.');
  }

  //省略

つまり、配列であっても0番目以外のフィールドには使えます。

バリデーションで連想配列ではなく配列を使いたいシーンがあるのかわからないですが、対策するならコアクラスを拡張して、 Validation::add()連想配列のみ通す、または Fieldset::add()Validation::_empty()みたいに 0 と '0' を通すのどちらかですかね。

参考

Validation クラス

*1:ラジオボタン使えというのは、とりあえず無しでおねがいします

*2:なんで Input::post じゃなくて Input::param なんでしょうね。

*3:1.7のドキュメントにはないけど、1.8のドキュメントのrun()の説明にある

我が家の読み聞かせ

はじめに

このエントリーは、お子さん、どんな本読んでる? Advent Calendar 2015 - Adventarの8日目です。

昨日の記事は、id:ohesotori さんの 復刊ドットコムと、赤羽末吉の「おへそがえるごん」 #adventcalender2015 - ここはちょっと見せられない でした。

仕事がひと段落したので、せっかくだから何かAdventCalendarに参加しようと思って探していたところ素敵なテーマを見つけたので参加しました。

珍しく家族ネタです。

2歳の息子が好きな本の中から、悩みに悩んで1冊だけ紹介します。

お気に入りの一冊

あかたろうの1・2・3の3・4・5 (おにのこあかたろうのほん 1)

あかたろうの1・2・3の3・4・5 (おにのこあかたろうのほん 1)

知人からお下がりでいただいた本で、Amazonによると1977年発売なので私よりもちょい年上の本です。

新しい本もたくさんあるのですが、年季の入ったこの本が、なぜかお気に入りです。

出版社の紹介文を引用します。

おにの子のあかたろうが外から帰ってくると、お母さんがいません。そこで、次から次へと電話をかけて、お母さんを追いかけます。 あかたろうの1・2・3の3・4・5 | 偕成社

お母さんを探して家中の部屋をのぞいたり、いろんなところに電話をかけるといった同じことを繰り返すところが楽しいみたいです。

あかたろうが部屋をのぞいてお母さんいないと言っているところでは「いなーい」、電話をすれば「でんわー」「もしもしー」、さかなやさんが出てくれば「さかなー」など、読み聞かせというよりは一緒になって楽しんでます。

よほどフレーズが気に入ってるようで、妻が息子を連れて友人宅にお邪魔した際もトイレのドアを開けて、お母さんいなーいとかやってたそうです。

読み聞かせについて

妻が読んであげることが多いですが、そこそこ早く帰れた平日や週末は私もやります。

ちょっと前までは読み聞かせをしようとしても絵本を自分で持ちたがり、こちらが読んでいてもお構いなしにページをめくりたがりました。
まだ内容が理解できず、目の前にある絵本というモノ自体に興味津々だったのでしょう。

いつの頃からか私や妻が話す間、手を出さずに聞いていられるようになってました。

最近では、お気に入りの本を持って私のところまで来ると、膝の上にちょこんと座って「おんでー(読んでー)」、読み終わると「もっかいおんでー」です。
これが数ターン続きます。
まあ、3回、4回となってくると、別のにしようよと思いますけど。

私が他のことをしていたり、本を手に取らなかったりすると腕をペシペシしながら怒ります。

怒ってるところも含めて、仕草と舌足らずなところががめっちゃかわいいです。

完全に親バカですね。

いつまでこんな風にせがんでくれるかはわからないけど、これからもたくさんの本を一緒に楽しみたいなと思ってます。

どっとはらい

PDOでMySQLに接続するとエラー発生

環境

事象

コード

事象を再現させるための実験コード。
といっても、普通にPDO接続するだけ。

<?php
define('DSN','mysql:host=localhost;dbname=hoge');
define('DB_USER','hoge');
define('DB_PASSWORD','hoge');

try {
  $dbh =  new PDO(DSN, DB_USER, DB_PASSWORD, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
} catch (PDOException $e) {
  echo $e->getMessage();
  exit;
}
結果
SQLSTATE[HY000] [2000] mysqlnd cannot connect to MySQL 4.1+ using the old insecure authentication. Please use an administration tool to reset your password with the command SET PASSWORD = PASSWORD('your_existing_password'). This will store a new, and more secure, hash value in mysql.user. If this user is used in other scripts executed by PHP 5.2 or earlier you might need to remove the old-passwords flag from your my.cnf file

原因

PHP5.3からの下位互換のない変更点に該当しているため。

新しい mysqlnd ライブラリは、MySQL 4.1 用の41バイトの新しいパスワードフォーマットを使用します。 古い16バイトのパスワードを使うと、mysql_connect() 系の関数は次のようなエラーメッセージを生成します。"mysqlnd cannot connect to MySQL 4.1+ using old authentication"

http://php.net/manual/ja/migration53.incompatible.php

要するにMySQLのパスワード設定が古いからのようだ。

対応

管理者ユーザでパスワードを再設定してやればOK。

SET SESSION old_passwords=0;
SELECT * FROM user WHERE user = 'hoge';
UPDATE user SET password = password('XXXXXXXXXXXX') WHERE user = 'hoge';

FuelPHPのUploadクラスを使用するときに注意すること

FuelPHPでファイルアップローダーを作ってた時にちょっとはまったこと。

バージョン

FuelPHP 1.7.2

現象

`upload_max_filesize`、`post_max_size`などを、1GBの設定にしても、400MBのファイルでエラーになる。

原因

FuelPHPのUploadクラスには個別の設定があり、そっちがデフォルトの300MBのままだった。

公式サイトのエラーの説明読んでてたらそれっぽいこと書いてあった。

Upload クラス

UPLOAD_ERR_MAX_SIZE The file uploaded exceeds the maximum file size defined in the configuration.

http://fuelphp.jp/docs/1.7/classes/upload/usage.html#/error_constants

対応

app/config/upload.phpの`max_size`を必要なサイズに変更すればOK。
PHPの設定値より大きくしても意味ないので注意。

	// maximum size of the uploaded file in bytes. 0 = no maximum
	'max_size'			=> 1024 * 1024 * 1024,

2014年振り返り

あっという間に終わった感があるけど、2014年の振り返りを。
今年はほとんどブログ書かなかったなー。

お仕事

1月から新しい職場で働き始めて、Webプログラマ見習いとしてPHPJavaScriptでお仕事してました。

コード書く仕事は半分くらいで、残りはプロジェクトの交通整理的な役割が多かったです。
それほど大きな会社ではないので、割と色々なことをやらせてもらえました。
プロジェクトを進める上では意外と今までの知識や経験が活かせることが多かった気がします。
前職と違った仕事のやり方として、Slackやチャットワークを日常的に使ってますがとてもいい感じです。

ちょうど1年経ちましたが、全体としてはそこそこ楽しく仕事できてます。

プライベート

子どもが1歳5ヶ月になり、どんどん動き回るようになって目がはなせません。
日々子どもが成長するのを見てるのが楽しいです。
子育てのためだけではないですが、勉強会にほとんど参加しませんでした。

自宅ではまとまった時間が取りにくいので、1時間早めに出社して本読んだり写経したりしてます。
おかげで、月1冊ちょっとは読めたみたいです。
慣れてきたのか、子育て手伝いながらの時間の使い方は去年よりは上手くなってるかも。

反省

新しい環境で仕事を始めたこともあって、インプットはそこそこ多くできた気がしますが、アウトプットは少なめでした。
職場と自宅という2つの環境に閉じてしまった感があるので、もう少し別の界隈の人たちとの交流ができたらよかったかな。

特定のファイルがないディレクトリを抽出する

仕事でサーバ設定いじってて、ちょっと悩んだのでメモっておく。
とりあえず目的は達したけど、もっと簡単にできる方法があれば、ぜひ教えてください。

やりたいこと

同じ構成のディレクトリ群の中から、置き忘れる、消されるなどして必要なファイルが存在しないディレクトリを抽出したい。

やったこと

対象ファイルについて存在チェックすれば良いと考えたので、下記のようにディレクトリのリストに対象のファイル名を連結して test コマンドに渡すようにした。
ここでは、hoge/piyo.txt が必要なファイル。
ディレクトリの構成によっては、find に maxdepth オプションも指定した方がいいはず。

[~/tmp/test] $ find .
.
./dir1
./dir1/fuga
./dir1/hoge
./dir1/hoge/piyo.txt
./dir2
./dir2/fuga
./dir2/hoge
./dir3
./dir3/fuga
./dir3/hoge
[~/tmp/test] $ find . -type d -mindepth 2|grep '/hoge$'|awk '{cmd = sprintf("test -e %s/piyo.txt", $0);r = system(cmd);if(r == 1) print $0}'
./dir2/hoge
./dir3/hoge
[~/tmp/test] $

JavaScript と CSS の minify

久々G*関連ネタ。

tree-tips: Gradleでjavascriptをminifyする! | Gradle
tree-tips: Gradleでcssをminifyする! | Gradle

上記サイトのビルドスクリプトを参考に、カレントディレクトリ以下の JavaScriptCSS再帰的に minify するようにした。

ただ、使っているプラグインが新しめの gradle に対応してないっぽくて、ちょっとはまった。
結局 1.7 です。

wrapper 作ったので、ついでにチームの人に紹介しておいた。

defaultTasks 'allMinifyCss', 'allMinifyJs'

apply plugin: 'js'
apply plugin: 'css'

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.eriwen:gradle-js-plugin:1.5.0'
    classpath 'com.eriwen:gradle-css-plugin:1.2.1'
  }
}

css.source {
  dev {
    css {
      srcDir "./"
        include "**/*.css"
        exclude "**/*.min.*"
    }
  }
}

css.source.dev.css.files.eachWithIndex { cssFile, i ->
  tasks.create(name: "minifyCss${i}", type: com.eriwen.gradle.css.tasks.MinifyCssTask) {
    source = cssFile
    dest = cssFile.getAbsolutePath().replace('.css','.min.css').replace('.js', '.min.js')
    yuicompressor {
      lineBreakPos = -1
    }
  }
}

task allMinifyCss(dependsOn: tasks.matching { Task task ->
    task.name.startsWith("minifyCss")
  }
)

javascript.source {
  dev {
    js {
      srcDir "./"
        include "**/*.js"
        exclude "**/*.min.js"
    }
  }
}

javascript.source.dev.js.files.eachWithIndex { jsFile, i ->
  tasks.create(name: "minifyJs${i}", type: com.eriwen.gradle.js.tasks.MinifyJsTask) {
    source = jsFile
    dest = jsFile.getAbsolutePath().replace('.js', '.min.js')
  }
}
task allMinifyJs(dependsOn: tasks.matching { Task task ->
    task.name.startsWith("minifyJs")
  }
)

task wrapper(type: Wrapper) {
  gradleVersion = 1.7
}