FuelPHPでFieldset_Builderを作ったら割とスッキリした


## はじめに
`FuelPHP`でFormを使った更新系の処理を書こうとしたときに、正直、どこにどうやって処理を書くべきか分からなかったので、結果、独自にBuilderクラスを作ってしまったという話です。

## `FuelPHP`にも`Fieldset`クラスがあるけれど
FuelPHPの公式ドキュメントでForm関連のドキュメントを確認してみます。

| クラス名 | 主な機能 |
| ----- | ----- |
| [Form](https://fuelphp.com/docs/classes/form.html) | HTMLのFORM（INPUT、SELECT等含む）要素をレンダリングする |
| [Fieldset](https://fuelphp.com/docs/classes/fieldset/fieldset.html) | フォーム要素の集合。バリデーション、モデルとのマッピング機能を持つ |

ぱっと見、フォーム固有の情報や処理は`Fieldset`に書けばいいのか、ってなるんですが、いろいろ検討してみた結果、Builderクラスを作るという結論に至ります。

- `Fieldset`は汎用的な機能のみを提供
- `Fieldset`の内部の実装を知る必要はない、かつ`public`なメソッドばかりなので、継承ではなく外部から利用するのが良さそう
- フォーム固有の情報や処理をaddしていくBuilder的なクラスが、各フォームごとにあれば良さそう

## 実装
### Fieldset_Builder
`fuel/app/classes/`配下に下記のようにファイルを作成します。

```
fuel/app/classes/fieldset
├── builder
│   └── example.php
│   └── interface.php
└── builder.php
```

```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();

}
```

```php
/**
 * Interface Fieldset_Builder_Interface
 */
interface Fieldset_Builder_Interface
{
	/**
	 * @return Fieldset_Builder_Interface
	 */
	public static function instance();

	/**
	 * @return Fieldset
	 */
	public function build();
}
```

```php
/**
 * 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`をビルド
- バリデーションの実行
- 保存処理とエラー処理

```php
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

```php
class Presenter_Frontend extends Presenter
{
	public function view()
    {
		Fieldset_Builder_Example::instance()->build();
    }
}
```

### Twig_Fuel_Extensionへのメソッド追加

```php
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`にて<tr>タグが含まれているので、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](https://fuelphp.com/docs/classes/validation/validation.html#/rules)独自記法便利そうなんだけど、使いこなせなさそうだったら、Callbackで書いてしまえばよい

## おわりに
なかなか独特な世界観でレールに乗りきれない`FuelPHP`ですが、とはいえ、使っていますという声はちょいちょい聞きます。
今回のような、ちょっとした実装でも、積極的にオープンにすることで誰かのお役に立てれば幸いです。
ではでは。