LoginSignup
6
2

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-27

はじめに

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クラスは、全てのインスタンスを管理していて、名前で区別している
  • twigFieldsetオブジェクトを渡すと、renderされた文字列に変換されてしまう
  • Twigエクステンション側に、Fieldsetの名前を渡すことで、任意のFieldsetを取得し、各フィールドをレンダリングする
  • fuel/core/config/form.phpfieldset_templatefield_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的な専用のクラスを作ってもいいかもしれない
    • FuelPHPValidation rules独自記法便利そうなんだけど、使いこなせなさそうだったら、Callbackで書いてしまえばよい

    おわりに

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

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2