fuelphpのfieldsetで入力フォームを高速かつエレガントかつワイルドに量産してみる


こんにちは。
開発チームのワイルド担当、まんだいです。
弊社では、ウェブアプリケーションの構築でfuelphpを利用することが多いのですが、ちまちまフォームを作ってたら手が足りないぜという事態に陥ったので、ドカンとfieldsetを利用して量産体制を行ったところ、大幅に工数の削減ができ、非常に満足している今日この頃です。
今回は、そんなfieldsetのエッセンスをお伝えできればと思います。
なお、FuelPHPのバージョンは1.7.2で確認しております。

 

fieldsetって何?

fieldsetこそ、fuelphpの高速開発の申し子じゃないかと思うのですが、意外にも深く追求した日本語記事が少ないように思います。
ちょっと使ってみた~みたいな記事はたくさんありますがね。
fieldsetは、モデルに定義した情報を元に、メソッド一発でフォームを作る仕組みで
標準的なtwitter bootstrapを利用しているウェブアプリケーションなら
多少目をつぶればHTMLを書くことなく、フォーム部分の出力が可能です。

モデルさえ丁寧に書いてしまえば、後はfieldsetの勢いに身を任せて一気に作る事ができますので
入力画面の枚数が多くて、しばらく終電確定の方はぜひご検討いただいて、家族との時間を捻出してもらえれば幸いです。

 

じゃあモデルでも書いてみようか

fieldsetを使う場合、フォーム生成に必要な情報は、全て$_propertiesの中に押し込んでしまいます。
inputのtypeはもちろん、バリデーションもここに書いて、フォームの情報を一元管理する事ができます。
試しに、本の登録画面を想定したモデルを例に、Model_Bookクラスを作ってみます。

booksテーブルは以下のようなものを用意しました。

CREATE TABLE `books` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `title` varchar(512) NOT NULL,
  `author` varchar(256) NOT NULL,
  `price` int(10) unsigned NOT NULL,
  `isbn` varchar(13) DEFAULT '',
  `released_at` date DEFAULT '0000-00-00',
  `type` tinyint(2) DEFAULT NULL,
  `created_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  `updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 
このbooksテーブルに対するモデルを以下のように定義します。

<?php
// fuel/app/classes/model/book.php

class Model_Book extends \Model\Orm
{
    protected static $_connection = 'default';
    protected static $_table_name = 'books';
    
    protected static $_properties = [
        'id',
        'title' => [
            'data_type' => 'varchar',
            'label' => 'タイトル',
            'validation' => [
                'required',
                'max_length' => [512],
            ],
            'form' => [
                'type' => 'text', 
                'class' => 'form-control',
            ],
        ],
        'author' => [
            'data_type' => 'varchar',
            'label' => '著者',
            'validation' => [
                'required',
                'max_length' => [512],
            ],
            'form' => [
                'type' => 'text', 
                'class' => 'form-control',
            ],
        ],
        'price' => [
            'data_type' => 'int',
            'label' => '値段',
            'validation' => [
                'required',
                'numeric_min' => [0],
            ],
            'form' => [
                'type' => 'number', 
                'class' => 'form-control',
                'min' => 0,
            ],
        ],
        'isbn' => [
            'data_type' => 'varchar',
            'label' => 'ISBN',
            'validation' => [
                'match_pattern' => ['/^[0-9]{10}([0-9]{3})?$/'],
            ],
            'form' => [
                'type' => 'text', 
                'class' => 'form-control',
            ],
        ],
        'released_at' => [
            'data_type' => 'date',
            'label' => '発行日',
            'validation' => [
                'valid_date' => [],
            ],
            'form' => [
                'type' => 'text', 
                'class' => 'form-control datepicker',
            ],
        ],
        'type' => [
            'date_type' => 'tinyint',
            'label' => '判型',
            'validation' => [
                'numeric_min' => [0],
            ],
            'form' => [
                'type' => 'select', 
                'class' => 'form-control',
                'options' => [
                    0 => '不明',
                    1 => 'A4判',
                    2 => 'A5判',
                    3 => 'A6判',
                    4 => 'B4判',
                    5 => 'B5判',
                    6 => 'B6判',
                    7 => '小B6判',
                    8 => '菊倍判',
                    9 => '国際判',
                    10 => 'AB判',
                    11 => '重箱版',
                    12 => '菊判',
                    13 => '四六判',
                    14 => 'B40判',
                    15 => 'ポケット・ブック版',
                    16 => '三五判',
                    17 => '八折り判',
                    18 => 'HL判',
                ],
            ],
        ],
        'created_at' => [
            'form' => ['type' => false],
        ],
        'updated_at' => [
            'form' => ['type' => false],
        ],
    ];
    
    protected static $_observers = [
        'Orm\Observer_CreatedAt' => [
            'events' => ['before_insert'],
            'mysql_timestamp' => true,
        ],
        'Orm\Observer_UpdatedAt' => [
            'events' => ['before_update'],
            'mysql_timestamp' => true,
        ],
    ];
}

 
これで必要な情報はだいたい埋まりました。
非常にコンパクトに情報がまとまり、構造的な配列で書けるので、間違いも少なく済みますし
後からの修正も簡単です。
標準で定義されているバリデーションルールはValidation - クラス - FuelPHP ドキュメントに載っていますのでありがたく使わせていただきましょう。

フォーム作成に関する情報は、各フィールドの「form」キー以下に記載します。
「type」はinputタグのtype属性に相当し、普段使っている、text、select、hidden、checkbox、radioなどから、textareaタグも同じように扱えます。
type="select"や、type="checkbox"、type="radio"の場合、option要素を「options」以下に配列で書く事で、固定値を定義できます。

また、固定値ではない場合、以下のようにコントローラ側からoptionsにデータをセットする事ができます。

$form = \Fieldset::forge('default', [
    'form_attributes' => ['class' => 'form-horizontal']
])->add_model('Model_Book');

$newspaper_types = [
    19 => 'ブロードシート判',
    20 => 'ノルディッシュ判',
    21 => 'レニッシュ判',
    22 => 'スイス判(NZZ判)',
    23 => 'ベルリナー判',
    24 => 'タブロイド・エクストラ判',
    25 => 'ハーフ・スイス判',
    26 => 'ハーフ・ベルリナー判',
    27 => 'ハーフレニッシュ判',
    28 => 'ハーフ・ブロードシート判',
    29 => 'タブロイド判',
];

// 項目を追加する場合
$form->field('type')->set_options($newspaper_types);

// 項目をリプレイスする場合
$form->field('type')->set_options($newspaper_types, null, true);

 
set_optionsメソッドの第三引数にtrueをセットすると、配列をマージせず上書きします。
ひとまず、モデル側では未選択状態の値だけ入力しておき、その他に取り得る値がある場合は値を追加、みたいな事が簡単にできます。

$form->field('type')でフォームの要素を直接操作できるので、この形さえ覚えていれば、ある時だけ少しだけ表現を変えたい場合も
コントローラで制御できます。
$form->field('type')に繋げて色々操作するメソッドは、fuel/core/classes/fieldset/field.php にあります。
メソッドも合わせて見てみると目的にあったメソッドが見つかると思います。

 

メソッド一発でフォームを作る

モデルの定義ができたら、後はコントローラで微調整をし、HTMLを生成するだけです。
微調整というのはどういうことかと言うと、何も手を入れていない状態だと、submitボタンがないフォームが出来てしまうという事に対する調整です。
フォームなんだから、submitボタンは必須だろ?と思うかも知れませんが
余計なお世話はしない、cakephpとは違った哲学を持つfuelphpならではかな、とも思えます。

$form->add('submit', ' ', ['type'=>'submit', 'class'=>'btn btn-primary submit', 'value' => '登録']);

 
とはいえ、これだけなので、ほぼコピペで対応可能です。

最後に、Viewにフォーム部分のHTMLを渡す訳ですが
Controller_Template を継承したコントローラの場合、以下の様な段取りでViewをforgeし、formを組み立てるbuildメソッドの出力を流し込むだけです。

$this->template->content = View::forge('manager/master/form.php', $this->contents);
$this->template->content->set_safe('form', $form->build());

 
fuelphpは、viewへ渡す文字列を自動的にエスケープします。
しかし、set_safeメソッドは、第二引数の情報をエスケープせずにViewへ渡す際に使います。
セキュリティ上はあまり使わない方が良いかも知れませんが、これ以外にうまい方法は現状なさそうなのでこうしています。

 

bootstrapが綺麗に適用されるフォームを作りたい

最初の方で、多少目をつぶれば小マシなフォームは作れる・・・と書きましたが
見栄えはそれでも大事にしたいのが人情です。よね!
デフォルトの状態では、テーブルの中にラベルとinput要素が並べられているフォームが表示されるかと思いますが
テンプレートを操作する事で、この挙動を制御する事が可能です。

デフォルトのテンプレートは、fuel/core/config/form.php になっていますので、このファイルをapp/config/以下にコピーして修正していきます。

例として、twitter bootstrapのHorizontal formが適用されるよう修正したテンプレートconfigファイルの例を示します。

<?php
// fuel/app/config/form.php

return [
    'form_template'              => "\n\t\t{open}\n{fields}\n\t\t{close}\n",
    'field_template'             => "\t\t<div class=\"form-group\">\n\t\t\t{label}\n\t\t\t<div class=\"col-xs-3 {error_class}\">{field}<span>{description}</span> {error_msg}</div>\n\t\t</div>\n",
    'multi_field_template'       => "\t\t<div class=\"form-group\">\n\t\t\t<label class=\"col-xs-2 control-label\">{group_label}{required}</label>\n\t\t\t<div class=\"col-xs-3 {error_class}\">{fields}\n\t\t\t\t<div>{field} {label}</div>{fields}<span>{description}</span>\t\t\t{error_msg}\n\t\t\t</div>\n\t\t</div>\n",
    'label_class'                => 'col-xs-2 control-label',
    'group_label'                 => '{label}',
];

 
\tや\nなどが混在して掴みにくいフォーマットになっていますが、画面表示には関係ないものなので省いてしまっても問題はありません。HTMLソースが多少見辛くなるだけです。
また、デフォルトのcore/config/form.php に載っていてapp/config/form.phpに載っていない項目もありますが、それはマージされてデフォルトの設定が利用されるので問題ありません。

また、twitter bootstrap派生のAdminLTEも問題なく使用できます。

 

実際どれほど工数が削減できるか

まず前提条件として、DB設計が(ある程度)終わっている必要があるのは、モデルに情報を集中させるという点から見ても明らかです。
fieldsetを利用した画面作成の場合、DB設計と並行して実装を進めるのは無理があるので、まず最初にDB設計を確定させる事に注力すべきです。

また、fieldsetの仕組みや、ベストプラクティス的なものを調べながら進めた場合、序盤にコストがかかりますが、熟考した分、慣れてきた後半のスピード感は、もはや官能的とも言えるレベルで劇的に早く終わったんじゃないかと思います。
実際、予定していた工数を大きく残して、余った時間(おいおい)でブログを書いているわけですし。

未体験の方は、この便利さに触れてみてはいかがでしょうか。

 
以上です。


この記事をかいた人

About the author

萬代陽一

ソーシャルゲームのウェブ API などの開発がメイン業務ですが、ありがたいことにマーケティングなどいろんな仕事をさせてもらえています。
なおビヨンド内での私の肖像権は CC0 扱いになっています。