はじめに
FuelPHP
でFormを使った更新系の処理を書こうとしたときに、正直、どこにどうやって処理を書くべきか分からなかったので、結果、独自にBuilderクラスを作ってしまったという話です。
FuelPHP
にもFieldset
クラスがあるけれど
FuelPHPの公式ドキュメントでForm関連のドキュメントを確認してみます。
クラス名 | 主な機能 |
---|---|
Form | HTMLのFORM(INPUT、SELECT等含む)要素をレンダリングする |
Fieldset | フォーム要素の集合。バリデーション、モデルとのマッピング機能を持つ |
ぱっと見、フォーム固有の情報や処理はFieldset
に書けばいいのか、ってなるんですが、いろいろ検討してみた結果、Builderクラスを作るという結論に至ります。
-
Fieldset
は汎用的な機能のみを提供 -
Fieldset
の内部の実装を知る必要はない、かつpublic
なメソッドばかりなので、継承ではなく外部から利用するのが良さそう - フォーム固有の情報や処理をaddしていくBuilder的なクラスが、各フォームごとにあれば良さそう
実装
Fieldset_Builder
fuel/app/classes/
配下に下記のようにファイルを作成します。
fuel/app/classes/fieldset
├── builder
│ └── example.php
│ └── interface.php
└── builder.php
/**
* Class Fieldset_Builder
*/
abstract class Fieldset_Builder implements Fieldset_Builder_Interface
{
/** @var Fieldset_Builder_Interface[] */
private static $instances;
/** @var string */
protected static $name;
/**
* @return Fieldset_Builder_Interface
*/
public static function instance()
{
if (empty(self::$instances[static::$name])) {
$instance = new static();
self::$instances[static::$name] = $instance;
}
return self::$instances[static::$name];
}
/**
* {@inheritdoc}
*/
public function build()
{
$fields = Fieldset::forge(static::$name);
$methods = $this->get_methods();
foreach ($methods as $method) {
$method($fields);
}
return $fields;
}
/**
* @return callable[]
*/
abstract protected function get_methods();
}
/**
* Interface Fieldset_Builder_Interface
*/
interface Fieldset_Builder_Interface
{
/**
* @return Fieldset_Builder_Interface
*/
public static function instance();
/**
* @return Fieldset
*/
public function build();
}
/**
* Class Fieldset_Builder_Example
*/
class Fieldset_Builder_Example extends Fieldset_Builder
{
protected static $name = 'example';
/**
* {@inheritdoc}
*/
protected function get_methods()
{
return [
[self::class, 'add_name'],
[self::class, 'add_email'],
[self::class, 'add_password'],
];
}
/**
* @param Fieldset $fields
*/
protected static function add_name($fields)
{
$fields
->add('name', 'name', [
'type' => 'text'
])
->add_rule(function ($value) {
return !empty($value);
})
->set_error_message(null, 'Please input your name.')
->set_template('{field} {description} {error_msg}')
;
}
/**
* @param Fieldset $fields
*/
protected static function add_email($fields)
{
$fields
->add('email', 'email', [
'type' => 'email'
])
->add_rule(function ($value) {
return !empty($value);
})
->set_error_message(null, 'Please input your email.')
->set_template('{field} {description} {error_msg}')
;
}
/**
* @param Fieldset $fields
*/
protected static function add_password($fields)
{
$fields
->add('password', 'password', [
'type' => 'password'
])
->add_rule(function ($value) {
return !empty($value);
})
->set_error_message(null, 'Please input your password.')
->set_template('{field} {description} {error_msg}')
;
}
}
Controller
コントローラー側では、FormからPOSTされた値をDBに保存する処理を実行する。
-
Fieldset_Builder
インスタンスからFieldset
をビルド - バリデーションの実行
- 保存処理とエラー処理
class Controller_Example extends Controller
{
/**
* @return mixed
*/
public function post()
{
$fields = Fieldset_Builder_Example::instance()->build();
$validation = $fields->validation();
if ($validation->run()) {
$input = $validation->input();
if (!$this->save($input)) {
self::set_flash_error($validation, ['Failed to save.']);
}
} else {
self::set_flash_error($validation);
}
Response::redirect('/home');
}
/**
* @param array $input
* @throws Exception
* @return bool
*/
private function save($input)
{
$db = Database_Connection::instance();
try {
$db->start_transaction();
/** 登録処理(省略)*/
return $db->commit_transaction();
} catch (Exception $e) {
if ($db->in_transaction()) {
$db->rollback_transaction();
}
throw $e;
}
return false;
}
/**
* @param Validation $validation
* @param array|null $messages
*/
private static function set_flash_error($validation, $messages = null)
{
$error = [
'input' => $validation->input(),
'message' => $messages === null ? $validation->error_message() : $messages
];
Session::set_flash('form_example_error', $error);
}
}
Presenter
class Presenter_Frontend extends Presenter
{
public function view()
{
Fieldset_Builder_Example::instance()->build();
}
}
Twig_Fuel_Extensionへのメソッド追加
use Parser\Twig_Fuel_Extension;
class Twig_Common_Extension extends Twig_Fuel_Extension
{
/** 省略 */
/**
* @param string $fieldset
* @param string $field
* @param $value
* @param array $attributes
* @return string
*/
public function form_field($fieldset, $field, $value = null, $attributes = null)
{
$fieldset_field = Fieldset::instance($fieldset)->field($field);
if (!is_null($value)) {
$fieldset_field->set_value($value, true);
}
if (!empty($attributes)) {
foreach ($attributes as $name => $attribute) {
$fieldset_field->set_attribute($name, $attribute);
}
}
return $fieldset_field->build();
}
{# fuel/app/views/example.twig #}
{{ form_field('example', 'name', name) }}
{{ form_field('example', 'email', email) }}
{{ form_field('example', 'password', password) }}
実装のポイント
なぜ、Twigのエクステンションが必要か
- Fieldsetクラスは、全てのインスタンスを管理していて、名前で区別している
-
twig
にFieldset
オブジェクトを渡すと、renderされた文字列に変換されてしまう - Twigエクステンション側に、Fieldsetの名前を渡すことで、任意のFieldsetを取得し、各フィールドをレンダリングする
-
fuel/core/config/form.php
のfieldset_template
、field_template
にてタグが含まれているので、configを上書きするか、Fieldset_Field#set_template
で、テンプレートを指定しないと<table>
レイアウトじゃないのに、<tr>
タグ付きでレンダリングされてしまう// fuel/core/config/form.php return array( // regular form definitions /** 中略 */ 'form_template' => "\n\t\t{open}\n\t\t<table>\n{fields}\n\t\t</table>\n\t\t{close}\n", 'fieldset_template' => "\n\t\t<tr><td colspan=\"2\">{open}<table>\n{fields}</table></td></tr>\n\t\t{close}\n", 'field_template' => "\t\t<tr>\n\t\t\t<td class=\"{error_class}\">{label}{required}</td>\n\t\t\t<td class=\"{error_class}\">{field} <span>{description}</span> {error_msg}</td>\n\t\t</tr>\n", 'multi_field_template' => "\t\t<tr>\n\t\t\t<td class=\"{error_class}\">{group_label}{required}</td>\n\t\t\t<td class=\"{error_class}\">{fields}\n\t\t\t\t{field} {label}<br />\n{fields}<span>{description}</span>\t\t\t{error_msg}\n\t\t\t</td>\n\t\t</tr>\n", 'error_template' => '<span>{error_msg}</span>', /** 中略 */ );
Fieldset_Builderの実装について
- Fieldsetと同じ名前でインスタンスを管理するようにしたが、必ずしも必要ではないので、Fieldset_Builderのインスタンスは管理しなくてもいいかもしれない
- Callableの配列を渡したのは、一つのメソッドで一つの
Fieldset_Field
の設定をするのがいいと思ったからで、Callableではなくて、Fieldset_Field_Builder
的な専用のクラスを作ってもいいかもしれない -
FuelPHP
のValidation rules独自記法便利そうなんだけど、使いこなせなさそうだったら、Callbackで書いてしまえばよい
おわりに
なかなか独特な世界観でレールに乗りきれない
FuelPHP
ですが、とはいえ、使っていますという声はちょいちょい聞きます。
今回のような、ちょっとした実装でも、積極的にオープンにすることで誰かのお役に立てれば幸いです。
ではでは。