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

[Symfony Advent Calendar 2016 12日目の記事。](http://qiita.com/advent-calendar/2016/symfony)

### ~~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などして変更することは可能でした


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

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

> <img width="203" alt="スクリーンショット 2016-12-11 21.55.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/131778/0074b3cd-f724-6d68-b727-834302f8013e.png">

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

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

各イベントでやること

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

```php

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;
    }
}
```

#### 成功
> <img width="222" alt="スクリーンショット 2016-12-11 21.56.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/131778/d8982633-3767-89f4-7691-3f0259b3a3a5.png">

#### 失敗
> <img width="503" alt="スクリーンショット 2016-12-11 21.56.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/131778/62a86e72-3ddb-d705-99ee-163f575672b8.png">

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

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

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

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