symfonyのValidatorで全角/半角変換などを行う

よくフォームで、全角で入力してくださいだとか、ひらがなで入力してくださいとか色々ありますが、UI的にはプログラム側である程度変換してあげるほうが望ましいですよね。

方法1: Eventを使う

symfony 1.3以降では form.filter_values というイベントが定義されています。これが呼ばれるのがFormのdoBindメソッドの先頭です。$form->bind($values) として実行してから、内部でバリデーションをする直前にイベントが発生します。

<?php
class RegisterForm extends sfFormSymfony
{
  public function setup()
  {
    $this->setValidators(array(
      'name'      => new sfValidatorString(),
      'name_kana' => new sfValidatorString(),
    ));

    self::$dispatcher->connect('form.filter_values', array(get_class($this), 'filterMultibyte'));
  }

  static public function filterMultibyte(sfEvent $event, $values)
  {
    $values['name_kana'] = mb_convert_kana($values['name_kana'], 'KVC', sfConfig::get('sf_charset', 'UTF-8'));

    return $values;
  }
}

$form = new RegisterForm();

$taintedValues = array('name' => '小川雄大', 'name_kana' => 'おがわかつひろ');
$form->bind($taintedValues);

var_dump($form->getValues());
//array(2) {
//  ["name"]=>
//  string(12) "小川雄大"
//  ["name_kana"]=>
//  string(21) "オガワカツヒロ"
//}

カナを("全角かな"、"半角かな"等に)変換する

PHP: mb_convert_kana - Manual

もうちょっと使い回しを意識した拡張がこちら:

<?php
/**
 * BaseForm
 */
class BaseForm extends sfFormSymfony
{
  public function __construct($defaults = array(), $options = array(), $CSRFSecret = null)
  {
    parent::__construct($defaults, $options, $CSRFSecret);

    self::$dispatcher->connect('form.filter_values', array(get_class($this), 'filterMultibyte'));
  }

  public function getMultibyteFields()
  {
    return array();
  }

  static public function filterMultibyte(sfEvent $event, $values)
  {
    $form = $event->getSubject();
    $fields = $form->getMultibyteFields();

    foreach ($fields as $field => $type) {
      $values[$field] = self::doFilterMultibyte($field, $values[$field], $type);
    }

    return $values;
  }

  static public function doFilterMultibyte($field, $value, $type)
  {
    if ($type === 'katakana') {
      $value = mb_convert_kana($value, 'KVC', sfConfig::get('sf_charset', 'UTF-8'));
    } else {
      $value = mb_convert_kana($value, $type, sfConfig::get('sf_charset', 'UTF-8'));
    }

    return $value;
  }
}

/**
 * RegisterForm
 */
class RegisterForm extends BaseForm
{
  public function setup()
  {
    $this->setValidators(array(
      'name'      => new sfValidatorString(),
      'name_kana' => new sfValidatorString(),
    ));
  }

  public function getMultibyteFields()
  {
    return array(
      'name_kana' => 'katakana',
    );
  }
}

doFilterMultibyte()メソッドの呼び出し部分をstatic(PHP 5.3以降で実装された遅延静的束縛)にすればdoFilterMultibyte()をオーバーライド可能です。

BaseFormを継承したクラスならgetMultibyteFields()を定義するだけで変換できるようになります。このような形でEventを使ってフィルタリングする方法があります。

方法2: フィルタリング用のバリデータを作成する

<?php
class myValidatorFilterMultibyte extends sfValidatorPass
{
  protected function configure($options = array(), $messages = array())
  {
    $this->addOption('convert_kana', 'KVsa');
  }

  protected function doClean($value)
  {
    if ($this->hasOption('convert_kana')) {
      $value = mb_convert_kana($value, $this->getOption('convert_kana'), $this->getCharset());
    }

    return $value;
  }
}

class RegisterForm extends sfFormSymfony
{
  public function setup()
  {
    $this->setValidators(array(
      'name'      => new sfValidatorString(),
      'name_kana' => new sfValidatorAnd(array(
        new myValidatorFilterMultibyte(array('convert_kana' => 'KVC')),
        new sfValidatorString(),
      )),
    ));
  }
}

Formではbind()メソッドを実行すると、設定しているバリデータのclean()メソッドがそれぞれ呼び出されて、正しければ値を返す、エラーであればsfValidatorError例外をthrowするということが行われます。

ここで、値を返す際に好きな形式で値を返すことが可能です。このタイミング、つまりバリデーションの最中に行うのがこの方法です。まあもうちょっとやり方はあるでしょうけど。

あとはいっそmyValidatorMbStringクラスを作って、doClean()をオーバーライドして、コンバートしたあとにparent::doClean()を呼び出すとか?どのみちバリデータの設定が必要なのでまあ面倒ですね。


個人的には方法1で書いたEventを活用する方法が楽かなあと思います。ただしフォームでイベントが使えるのは1.3以降なので1.2の場合はbind()をオーバーライドするとか面倒なことをしなければなりませんね。。。

とりあえず、form.filter_valuesは何かと使い道のありそうなEventなので覚えておくと幅が広がると思います。

追記

これプラグイン化できそうですね。暇なときに作ってみよう。jpFormMultibyteFilterPluginとか?