Form Collection とイベント周りについて

  • 10
    いいね
  • 0
    コメント

Symfony Advent Calendar 2016 12日目の記事。

Form Collection とイベント周りについて

Formイベント周りについて

Collectionについても書こうと思っていましたが、間に合わず断念m(_ _)m

というわけで、Formのイベント周りについてまとめてみました。

SymfonyのFormにはイベントというものが用意されており、
各イベントで、Formのモデルを成形したり、Formを動的に追加・編集・削除したり、ユーザーからのリクエストデータを成形したりなどを行なうことが可能です。

イベント一覧

Event Formの操作 Data 得意なこと
PRE_SET_DATA Model モデルの成形
POST_SET_DATA Model Formの成形 
PRE_SUBMIT Request Data リクエストデータの成形。Formの成形
SUBMIT Model データ検証。Formの成形
POST_SUBMIT Model 検証後の操作

追記 SUBMITの時もFormをaddなどして変更することは可能でした

イベントは上記のような5種類が定義されており、いい感じに利用することで、いい感じになります

ということで、実装例を書いてみます。
内容は

スクリーンショット 2016-12-11 21.55.58.png

画像のようなフォームを用意

  • 演算子 [+ - * /]
  • 値1
  • 値2
  • 値3

各イベントでやること

  • PRE_SET_DATA
    • 値123の初期値を設定
  • POST_SET_DATA
    • 送信ボタンを追加
  • PRE_SUBMIT
    • 入力された値3を半分の数値にする
  • SUBMIT
    • 値123が数値であることを確認
    • 値123で計算し、エラーが出ないことを確認
  • POST_SUBMIT
    • Formを検証し、エラーが無ければ、計算結果を出力
    • Formを検証し、エラーであればFailedを出力

class SampleType extends AbstractType
{
    // 演算の一覧
    const OPERATION_PLUS = 1;
    const OPERATION_MINUS = 2;
    const OPERATION_MULTIPLIED = 3;
    const OPERATION_DIVIDED = 4;

    const operations = [
        self::OPERATION_PLUS => '+',
        self::OPERATION_MINUS => '-',
        self::OPERATION_MULTIPLIED => '*',
        self::OPERATION_DIVIDED => '/',
    ];

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('operation', ChoiceType::class, [
                'label' => '演算子',
                'choices' => self::operations
            ])
            ->add('num1', TextType::class, [
                'label' => '値1'
            ])
            ->add('num2', TextType::class, [
                'label' => '値2'
            ])
            ->add('num3', TextType::class, [
                'label' => '値3 (強制的に半分)'
            ])
            ;

        // モデルが触れる
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            // 初期値を作成
            $data = $event->getData();
            $data['num1'] = 10;
            $data['num2'] = 20;
            $data['num3'] = 30;
            $data['operation'] = self::operations[1];
            $event->setData($data);
        });

        // モデルとフォームが触れる フォームを追加、編集を行う場合はここ
        $builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
            // サブミットボタンを追加
            $form = $event->getForm();
            $form->add('submit', SubmitType::class);
        });
        // リクエストデータとフォームがさわれる。 リクエストデータの成形や、リクエストデータによってフォームを変更させることができる
        $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
            // num3というリクエストデータを半分にする
            $data = $event->getData();
            $num3 = $data['num3'];
            if (is_numeric($num3) and $num3 > 0) {
                $data['num3'] = $num3 / 2;
            }
            $event->setData($data);
        });

        // リクエストデータがmappedされたモデルがさわれる。独自のValidation等で使う
        $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
            // 検証
            $form = $event->getForm();

            // 入力された値が数字かどうか
            if (!is_numeric($form->get('num1')->getData())) {
                $form->get('num1')->addError(new FormError('num1 not numeric!'));
            }
            if (!is_numeric($form->get('num2')->getData())) {
                $form->get('num2')->addError(new FormError('num2 not numeric!'));
            }
            if (!is_numeric($form->get('num3')->getData())) {
                $form->get('num3')->addError(new FormError('num3 not numeric!'));
            }

            // 計算に失敗するかどうか
            try {
                $this->calc($event->getData());
            } catch (ContextErrorException $e) {
                $form->addError(new FormError($e->getMessage()));
            }

        });

        // モデルが触れる。Validationを通った後にモデルを成形などに使える
        $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
            // 成功可否を確認し、成功であれば集計値を。 失敗であればFailedを出力
            $form = $event->getForm();
            $data = $event->getData();

            if ($form->isValid()) {
                print('SUCCESS Total: ' . $this->calc($data));
            } else {
                print('Failed');
            }
        });
    }

    /**
     * データを元に計算
     */
    private function calc($data)
    {
        $sum = false;
        switch ($data['operation']) {
            case self::OPERATION_PLUS:
                $sum = $data['num1'] + $data['num2'] + $data['num3'];
                break;
            case self::OPERATION_MINUS:
                $sum = $data['num1'] - $data['num2'] - $data['num3'];
                break;
            case self::OPERATION_MULTIPLIED:
                $sum = $data['num1'] * $data['num2'] * $data['num3'];
                break;
            case self::OPERATION_DIVIDED:
                $sum = $data['num1'] / $data['num2'] / $data['num3'];
                break;
        }

        return $sum;
    }
}

成功

スクリーンショット 2016-12-11 21.56.29.png

失敗

スクリーンショット 2016-12-11 21.56.40.png

あまりいい例が思いつかず、とても無理矢理なイベントの使い方をしているのは反省。

まとめ

今回のコードからは読み取りにくいとは思いますが、各イベントを上手く利用することで、面倒な作業をSymfonyに任せるようなことができ、すごく楽ができます。

もうすこし複雑なパターンでいくと、
PRE_SUBMIT時に特定のリクエストデータが来た場合Formのフィールドをdisabledにすることにより、値の更新を不可にしたりなど…

けっこうぐだぐだな感じでしたがこれで終わりにします。ありがとうございました