Help us understand the problem. What is going on with this article?

Symfony3でPOST値を一次元配列にしたい

More than 1 year has passed since last update.

概要

Symfony3でPOST値のルート配列の名前を変更したいときは FormFactory::createNamed() を使う。
GitHub公式リポジトリ:https://github.com/symfony/form/blob/3.4/FormFactory.php

経緯

EC-CUBE4のプラグイン開発中に、リンク型の決済で50を超えるリクエストパラメータを決済代行会社側のサーバにPOSTするという場面に出会った。

SymfonyのControllerで使う、 createForm() は、利用するFormTypeがフォームの名前になり、フォーム名の縦配列に実際にPOSTする値が横配列として入る二次元配列になる。

HogeController.php
$form = $this->createForm(HogeType::class, $Hoge);

return ['form' => $form->createView()];

POST
array:1 [▼
  "hoge" => array:2 [▼    // ← このhogeっていう親の配列が邪魔!!
    "key" => "value"
    "_token" => "TOKEN"
  ]
]

これはデータの紐づきをマッピングするためだと思われるが、今回のように所定のフォーマットに従ってリクエストしなければいけないときに困る。

テンプレートファイルにHTMLを直接書いてしまうという方法もありだが、今回のケースではリクエストパラメータが50を超え、受注の状態によっては連番で動的に出力しなければならないパラメータもあったため、なんとかFormTypeで対応したい。

hoge.twig
<input type="hidden" name="hoge" value={{ hoge }} />    // これでもいいのだが…
// 以下、最大で999項目(笑)
<input type="hidden" name="param1" value={{ value1 }} />
<input type="hidden" name="param2" value={{ value2 }} />
<input type="hidden" name="param3" value={{ value3 }} />
...

このため、name属性を上手くカスタマイズできないかといろいろ調べた結果、FormFactory::createNamed() に行き着いた。

使い方

第一引数に任意のフォーム名渡して、createFormと同じように使ってやればよい。

$form = $formFactory->createNamed('form_name', HogeType::class, $Hoge, [
  'action' => 'https://example.com'    // 第四引数もcreateFormの第三引数と同様に使える。
]);

今回は親の配列が邪魔なので、第一引数に空文字を渡してやる。

HogeController.php
$form = $this->formFactory->createNamed('', HogeType::class, $Hoge);
return ['form' => $form->createView()];
POST
array:2 [▼
  "key" => "value"
  "_token" => "TOKEN"
]

実装を読む

せっかくなので実装を読んでみた。
継承関係を追ってみると、

いつも使っている createForm()

HogeController→ (継承) -> Controller → (トレイト) -> ControllerTrait::createForm()

Symfony/Bundle/FrameworkBundle/Controller/Controller.php
Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php

ControllerTrait.php
protected function createForm($type, $data = null, array $options = [])
{
    return $this->container->get('form.factory')->create($type, $data, $options);
}

このまま追うと…:rolling_eyes:

FormFactory.php
public function create($type = 'Symfony\Component\Form\Extension\Core\Type\FormType', $data = null, array $options = [])
{
    return $this->createBuilder($type, $data, $options)->getForm();
}

public function createBuilder($type = 'Symfony\Component\Form\Extension\Core\Type\FormType', $data = null, array $options = [])
{
    if (!\is_string($type)) {
        throw new UnexpectedTypeException($type, 'string');
    }

    return $this->createNamedBuilder($this->registry->getType($type)->getBlockPrefix(), $type, $data, $options);
}

実は今回使ったcreateNamed() がラップしている createNamedBuilder() を更にラップしているだけだった!

一方、今回使った createNamed()

HogeController → (継承) → Controller → (DI?) → FormFactory::createNamed()

Symfony/Bundle/FrameworkBundle/Controller/Controllerphp
Symfony/Component/Form/FormFactory.php

FormFactory.php
public function createNamed($name, $type = 'Symfony\Component\Form\Extension\Core\Type\FormType', $data = null, array $options = [])
{
    return $this->createNamedBuilder($name, $type, $data, $options)->getForm();
}


public function createNamedBuilder($name, $type = 'Symfony\Component\Form\Extension\Core\Type\FormType', $data = null, array $options = [])
{
    if (null !== $data && !\array_key_exists('data', $options)) {
        $options['data'] = $data;
    }

    if (!\is_string($type)) {
        throw new UnexpectedTypeException($type, 'string');
    }

    $type = $this->registry->getType($type);

    $builder = $type->createBuilder($this, $name, $options);

    // Explicitly call buildForm() in order to be able to override either
    // createBuilder() or buildForm() in the resolved form type
    $type->buildForm($builder, $builder->getOptions());

    return $builder;
}

なるほど、スッキリした:relaxed:

あとがき

こうやって実装を読むのになれて、Core部分から柔軟に希望の実装を叶えるというテクニックも必要だなと思った。
SymfonyのDIがどういう仕組みで動いているかを知らないので、そのあたりの実装に迫ってみるのもありかもなと思っています。

指摘とかもっといい方法があったら指摘お願いします!!

宣伝

EC-CUBE、Laravelの開発お任せください!!

tokidrill
PHPやRubyやGoを書いてます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away