[!注意]2.3.0βの時点でORMの実装に問題があるようで、以下の記事で期待できる挙動を得るためにはENUMの要素に用いる文字に制限があります。ソースを確認できていませんが、小括弧'(‘, ‘)’や大括弧'[‘, ‘]’を用いると正しくフィールド定義を拾ってくれないようです。したがいまして、現時点でENUM型の定義から自動的にHTMLのoptionを描画させるためにはそれらの文字を使用しないことが前提になります。(2012/10/31)

ENUM型で楽をしたい!

そんな思いで、以前 scaffold の際にMySQLのENUM型を select の option 要素として扱う方法の一つとして、Scaffoldのビューをアプリケーションでオーバーライドする方法をご紹介しました。>CakePHP の Scaffold で ENUM を簡単に実装する方法

そこで今回は、FormHelper の input メソッドを拡張する方法です。

一応おさらいをしておきますと、ENUM型とSET型はSQLの標準仕様には含まれていないMySQLの独自拡張です。一方で、 CakePHP はデータベースの対応は「標準に準拠しますよ」と言うポリシーを貫いています。なので、これまでもそうだしこれからもCakePHPではMySQLのENUM型やSET型には対応しないでしょうねぇ、というのがだいたいの流れです。

何がしたいか

やりたいことは、MySQLでENUM型を定義して、FormHelper::input で名前だけ指定すると、使用可能な値を選択肢とした select タグのフォームを出力させることです。

これを実現させるために手を入れる対象は、モデル側でもコントローラ側でも構わないのですが、今回は簡単なのでFormHelperそのものを拡張することで対応しました。アプリケーションでFormHelperの継承クラスを作成して、それをコントローラ側でFormHelperの代替として使用します。

もうちょっと前置き

FormHelper::input($name, $options) の入力フォームの自動補完でselectフォームが生成されるパターンは二つあって、一つは $options パラメータに ‘options’ というキーで(ややこしい!)値がセットされている場合。もう一つはフィールド名(末尾に’_id’があればそれを除いた上で)の複数形の値がViewにセットされていた場合です。この後者の働きのおかげでhasManyアソシエーションを組んだモデルのデータが上手い按配でセットされたりしているのですね。

コード

さて、前置きが長くなりましたのでとりあえずコード貼り付けておきます。

<?php
App::uses('FormHelper', 'View/Helper');
/*
 * APP/View/Helper/MySqlEnumFormHelper.php
 * It extends FormHelper to implement ENUM datatype of MySQL.
 * CakePHP 2.2.3
 */ 
class MySqlEnumFormHelper extends FormHelper
{
    public function input($fieldName, $options = array())
    {
        if (!isset($options['type']) && !isset($options['options'])) {

            $modelKey = $this->model();

            if (preg_match(
                    '/^enum\((.+)\)$/ui',
                    $this->fieldset[$modelKey]['fields'][$fieldName]['type'],
                    $m
               )) {

                $match = trim($m[1]);
                $qOpen = substr($match, 0, 1); // ※1
                $qClose = substr($match, -1); // ※1
                $delimiter = $qOpen . ',' . $qClose;
                preg_match('/^' . $qOpen . '(.+)' . $qClose . '$/u', $match, $m);
                $_options = explode($delimiter, $m[1]);
                $options['type'] = 'select';
                $options['options'] = array_combine($_options, $_options);

            }
        }
        return parent::input($fieldName, $options);
    }
}

つまり、親クラスの input メソッドに渡す前に、引数にちょこっと手を加えてあげるというわけです。

※1) クォーテーションの文字は一応シングルクォーテーションしかなさそうなのですが、それを担保出来る文言や仕様書みたいなのを見つけられなかったので、少しだけ柔軟性を持たせています。フィールド定義値の取得に関して完全に理解していないため、マッチングの処理は全体的に冗長です。仕様を完全に理解していれば(たとえば前後に空白は絶対に入っていないとかクォーテーションは必ずシングルクォーテーションであるとか)もう少しスリムに書けるとは思います。

親メソッドと一部処理が重複しちゃってるので、気になる人はメソッドごとごっそり書き換えるのもありかも知れません。

で、これを FormHelper として使う場合は

<?php
App::uses('Controller', 'Controller');
class AppController extends Controller
{
    public $helpers = array(
        'Form' => array('className' => 'MySqlEnumForm'),
    );
}

とすると、View側からは、$this->Form でアクセスできるようになります。

副作用はほとんどないと思いますので、ENUM型を用いている場合はおすすめです。

以上で、たとえばこんなテーブル

CREATE TABLE IF NOT EXISTS `profiles` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `blood_type` enum('O','A','B','AB','unknown') NOT NULL
  PRIMARY KEY (`id`)
)

をこしらえた時、add.ctpにこんな風に書けば・・・

<?php
echo $this->Form->create(null, array('action'=>'add'));
echo $this->Form->input('blood_type');
echo $this->Form->end('submit');

すると、なんということでしょう。selectタグの完成です。

ところでENUM型ってあまり使われてないんですかね? やっぱりきちんと外部テーブル作ってアソシエーション張ってるんでしょうか。自分は結構好きで使ってるんですけど。

SET型はあきらめた!

ちなみに、ついでにSET型にも対応させようと思ったらこちらはカラム定義を全く拾ってくれてないことが判明したのでパスしました。ORM周りの拡張になりますのでちょっと大掛かりになりそうですし、自分自身SET型は使ったことが無いのであまりやる気が起きないです(笑)