フォーム

HTML のフォームを取り扱うことは、ウェブデベロッパーに取って、最も一般的で、チャレンジングなタスクの1つです。 Symfony はフォームを簡単に取り扱う為のフォームコンポーネントと総合されています。 この章では、1から複雑なフォームを構築します。そして、フォームライブラリの最も重要な機能を学びます。

Symfony の Form コンポーネントは、Symfony プロジェクト以外でも使用できる、単独のライブラリです。 詳細はコンポーネントのドキュメントを参照してください。

シンプルなフォームの作成

タスクを表示する必要がある、シンプルな TODO リストアプリケーションを構築しているとしましょう。 ユーザーがタスクを作成したり、編集する必要があるので、フォームを作成します。 始める前に、最初に1つのタスクを表して、データを保存する為の、一般的な Taskクラスに焦点を当てます。

// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;

class Task
{
    protected $task;
    protected $dueDate;

    public function getTask()
    {
        return $this->task;
    }

    public function setTask($task)
    {
        $this->task = $task;
    }

    public function getDueDate()
    {
        return $this->dueDate;
    }

    public function setDueDate(\DateTime $dueDate = null)
    {
        $this->dueDate = $dueDate;
    }
}

このクラスは “plain-old-PHP-object” です。なぜなら、今のところ、それは Symfony や他のライブラリと何もしていないからです。 それは、アプリケーション内で直接問題を解決する、完全にシンプルな普通の PHP オブジェクトです(例、アプリケーション内でタスクを表す)。 もちろん、この章の終わりまでには、Task インスタンスにデータを送信し(HTTP フォームを経由して)、そのデータを検証し、データベースに保存します。

フォームの組み立て

Task クラスを作成したので、次は実際の HTML フォームを作成し、表示します。 Symfony では、これは、フォームオブジェクトを構築することで実行します。そして、その時、テンプレート内でそれをレンダリングします。 今のところ、これは全てコントローラ内で実行できます。

// src/AppBundle/Controller/DefaultController.php
namespace AppBundle\Controller;

use AppBundle\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class DefaultController extends Controller
{
    public function newAction(Request $request)
    {
        // create a task and give it some dummy data for this example
        $task = new Task();
        $task->setTask('Write a blog post');
        $task->setDueDate(new \DateTime('tomorrow'));

        $form = $this->createFormBuilder($task)
            ->add('task', TextType::class)
            ->add('dueDate', DateType::class)
            ->add('save', SubmitType::class, array('label' => 'Create Task'))
            ->getForm();

        return $this->render('default/new.html.twig', array(
            'form' => $form->createView(),
        ));
    }
}

この例はコントローラ内で直接フォームを構築する方法を示します。 後ほど、フォームクラスの作成のセクションで、独立したクラスでフォームを構築する方法を学びます。 それは、フォームを再利用できる様にする、推奨される方法です。

Symfony のフォームオブジェクトはフォームビルダーを使って、構築されるので、フォームを作成することは、比較的少ないコードで済みます。 フォームビルダーの目的は、簡単なフォームレシピを記述できるようにし、実際のフォーム構築の困難な仕事を全て行うことです。 この例では、Task クラスの taskdueData プロパティに対応する、2つのフィールド taskdueData をフォームに追加しました。また、それぞれに、完全修飾クラス名によって表される型(例、TextTypeDateType)を割り当てました。 とりわけ、それは、フィールドに対して表示される HTML フォームタグを決定します。

最後に、サーバーにフォームを送信する為の送信ボタンをカスタムラベルと共に追加しました。

Symfony には、この後で軽く説明する、多くの組み込みタイプが付属しています(フィールドタイプを参照)。

フォームの表示

フォームが作成できたので、次はそれを表示します。 これは、テンプレートに特別なフォームビューオブジェクトを渡すことで行います(上記例の $form->createView() に注目してください)。 そして、フォームヘルパー関数のセットを使います。

{# app/Resources/views/default/new.html.twig #}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
<!-- app/Resources/views/default/new.html.php -->
<?php echo $view['form']->start($form) ?>
<?php echo $view['form']->widget($form) ?>
<?php echo $view['form']->end($form) ?>

フォーム

この例では、フォームが表示されたのと同じ URL へ、“POST” リクエストを使って、フォームを送信することを想定しています。 後で、フォームのリクエストメソッドとターゲット URL の変更方法を学びます。

これで終わりです。わずか3行で完璧なフォームが表示できました。

  • form_start(form)
    • form の開始タグを表示します。ファイルアップロードを使う時には、正しい enctype 属性を含みます。
  • form_widget(form)
    • 全てのフィールドを表示します。フィールド要素自身とフィールドの為のラベルやエラーメッセージを含みます。
  • form_end(form)
    • form の閉じタグを表示します。そして、各フォールドを手動で表示しているケースで、まだ表示されていない全てのフィールドを表示します。これは、hidden フィールドの表示や 自動的な CSRF 保護を利用するのに便利です。

簡単であればある程、柔軟ではなくなります(まだ)。大抵は、フォームがどの様に見えるかを制御するために、個々にフォームフィールドを表示したいと思います。テンプレートでのフォーム表示セクションで、その方法を学びます。

次に進む前に、$task オブジェクトの task プロパティの値(例、“ブログを書く”)を持つ task 入力フィールドがどの様に表示されているかに注目してください。オブジェクトからデータを取り出し、HTML フォームで表示する為の適切なフォーマットに変換することが、フォームの最初の仕事です。

フォームシステムは Task クラスの getTask()setTask()メソッドを通して、protected な task プロパティの値にアクセスできるように、賢くできています。 プロパティが public でない限り、フォームコンポーネントがプロパティのデータにアクセスできるように、ゲッターやセッターメソッドが必要になります。 boolean プロパティの為に、ゲッター(例、getPublished(), getReminder())の代わりに、“isser” または “hasser” メソッド(例、isPublished(), hasReminder())を使用することができます。

フォーム送信のハンドリング

フォームの2つめの仕事は、ユーザーから送信されたデータをオブジェクトのプロパティに変換することです。 これを行うために、ユーザーからの送信データを、フォームオブジェクト内に書き込む必要があります。 コントローラに次の機能を追加します。

// ...
use Symfony\Component\HttpFoundation\Request;

public function newAction(Request $request)
{
    // just setup a fresh $task object (remove the dummy data)
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', TextType::class)
        ->add('dueDate', DateType::class)
        ->add('save', SubmitType::class, array('label' => 'Create Task'))
        ->getForm();

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        // ... perform some action, such as saving the task to the database

        return $this->redirectToRoute('task_success');
    }

    return $this->render('default/new.html.twig', array(
        'form' => $form->createView(),
    ));
}

createView() メソッドは handleRequest が呼ばれた後に、呼ばれる必要があることに注意してください。 さもないと、*_SUBMIT イベントで行われた変更がビューに反映されません(バリデーションエラーのような)。

このコントローラはフォームをハンドリングする為の一般的なパターンに従っています。 そして、3つの経路を持っています。

  1. 最初にブラウザでページが表示された時、フォームはシンプルに作成されて、表示されます。handleRequest() は、フォームが送信されていないことを認識し、そして何もしません。isValid() は、フォームが送信されていない時は false を返します。

  2. ユーザーがフォームを送信した時、handleRequest() は、これを認識し、即座に送信されたデータを $task オブジェクトの taskdueDate プロパティに書き込みます。その時、このオブジェクトは検証されます。それが不正だった時は、isValid() は、再び false を返します。そして、フォームは全てのエラーメッセージと共に表示されます。
    送信されたデータの検証を行わずに、フォームが送信されたかどうかをチェックする為に、isSubmitted() メソッドを使うことができます。
  3. ユーザーが正しいデータでフォームを送信した時、送信されたデータは再びフォームに書き込まれます。しかし、今回は、isValid() は、true を返します。ここで、ユーザーを別のページ(例、成功メッセージを表示するページ等)にリダイレクトする前に、$task オブジェクトを使って何かのアクションを実行することができます(例、データベースに保存する等)。
    フォーム送信が成功した後に、ユーザーをリダイレクトすることで、ユーザーがブラウザで「再読み込み」ボタンを押して、再度、フォームデータを送信してくる事を防ぐことができます。

フォームが送信されたか、どのデータが渡されたかをより正確に制御したい時は、submit() を使います。 詳細は、クックブック内を参照してください。

複数のボタンを持つフォームの送信

フォームが1つ以上の送信ボタンを持っている時、コントローラ内のプログラムフローに当てはめる為に、どのボタンがクリックされたかをチェックする必要があります。これを行うには、フォームに見出しと共に、2つ目のボタンを追加します。

$form = $this->createFormBuilder($task)
    ->add('task', TextType::class)
    ->add('dueDate', DateType::class)
    ->add('save', SubmitType::class, array('label' => 'Create Task'))
    ->add('saveAndAdd', SubmitType::class, array('label' => 'Save and Add'))
    ->getForm();

コントローラ内で、“Save and add” ボタンがクリックされたかを問い合わせる為に、ボタンのisClicked() メソッドを使用します。

if ($form->isValid()) {
    // ... perform some action, such as saving the task to the database

    $nextAction = $form->get('saveAndAdd')->isClicked()
        ? 'task_new'
        : 'task_success';

    return $this->redirectToRoute($nextAction);
}

フォームバリデーション

前のセクションで、フォームがどの様に正しいデータや不正なデータと共に送信されてくるかを学びました。 Symfony では、検証はオブジェクト(例、Task)に対して適用されます。 つまり、フォームに送信データを適用した後、フォームが正しいかどうかではなく、$task オブジェクトが正しいかどうかを検証します。 $form->isValid() を呼び出すことは、$task オブジェクトが正しいかどうかを尋ねるショートカットです。

バリデーションはクラスにルール(制約)のセットを追加することで行います。 この動作を確認するには、「task フィールドは空でないこと」、「dueDate フィールドは空でないこと、DateTime オジェジェクトであること」といった制約を追加します。

// src/AppBundle/Entity/Task.php
namespace AppBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Task
{
    /**
     * @Assert\NotBlank()
     */
    public $task;

    /**
     * @Assert\NotBlank()
     * @Assert\Type("\DateTime")
     */
    protected $dueDate;
}
# src/AppBundle/Resources/config/validation.yml
AppBundle\Entity\Task:
    properties:
        task:
            - NotBlank: ~
        dueDate:
            - NotBlank: ~
            - Type: \DateTime
<!-- src/AppBundle/Resources/config/validation.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
        http://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd">

    <class name="AppBundle\Entity\Task">
        <property name="task">
            <constraint name="NotBlank" />
        </property>
        <property name="dueDate">
            <constraint name="NotBlank" />
            <constraint name="Type">\DateTime</constraint>
        </property>
    </class>
</constraint-mapping>
// src/AppBundle/Entity/Task.php
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;

class Task
{
    // ...

    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        $metadata->addPropertyConstraint('task', new NotBlank());

        $metadata->addPropertyConstraint('dueDate', new NotBlank());
        $metadata->addPropertyConstraint(
            'dueDate',
            new Type('\DateTime')
        );
    }
}

これだけです。不正なデータでフォームを再送信した時には、フォームに関連するエラーが表示されます。

HTML5 バリデーション

HTML5 では、多くのブラウザは、クライアント側で特定の検証制約を適用することができます。 最も一般的な検証は、必須の入力フィールドに require 属性をレンダリングする事により有効化されます。 HTML5 をサポートするブラウザでは、ユーザーが入力フィールドを未入力のまま、フォームを送信した時に、 ブラウザ側でエラーメッセージが表示されます。

生成されたフォームは、検証の引き金になる HTML 属性を適切に追加することで、この新しい機能の利点をフル活用します。 しかし、クライアント側の検証は、novalidate 属性を form タグに追加するか、formnovalidate を submit タグに追加することで、無効にすることができます。 これは、サーバ側の検証制約をテストしたいのに、空白フィールドの送信をブラウザが阻止しているような時に、特に便利です。

{# app/Resources/views/default/new.html.twig #}
{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
<!-- app/Resources/views/default/new.html.php -->
<?php echo $view['form']->form($form, array(
    'attr' => array('novalidate' => 'novalidate'),
)) ?>

バリデーションは Symfony の非常に強力な機能です。そして、それ専用の章があります。

バリデーショングループ

バリデーショングループを使用したい時は、フォームが使用するバリデーショングループを指定する必要があります。

$form = $this->createFormBuilder($users, array(
    'validation_groups' => array('registration'),
))->add(...);

フォームクラスを作成している場合、次のように configureOptions() メソッドを追加する必要があります。

use Symfony\Component\OptionsResolver\OptionsResolver;

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => array('registration'),
    ));
}

これらの両方で、registration バリデーショングループだけが、オブジェクトの検証に使用されます。

バリデーションの無効化

フォームの全ての検証を無効にすると便利な時があります。 この様な時は、validation_groups オプションに false をセットします。

use Symfony\Component\OptionsResolver\OptionsResolver;

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => false,
    ));
}

これを行う時には、フォームがまだ、基本的な整合性チェックを行うことに注意してください。 例えば、アップロードされたファイルが大きすぎないかどうかや、存在しないフィールドが送信されたがどうか等です。 検証を無効にする時、POST_SUBMIT イベントを使用することもできます。

送信データに基づくグループ

バリデーショングループを決めるための高度なロジックが必要な場合、validation_groups オプションにコールバックへの配列をセットすることができます。

use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => array(
            'AppBundle\Entity\Client',
            'determineValidationGroups',
        ),
    ));
}

これは、フォームが送信された後、検証が行われる前に、Client クラス上の静的メソッド determineValidationGroups() が呼ばれます。 フォームオブジェクトがそのメソッドの引数として渡されます(次の例を参照)。 また、クロージャーを使って、全体のロジックをインラインで定義することができます。

use AppBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function (FormInterface $form) {
            $data = $form->getData();

            if (Client::TYPE_PERSON == $data->getType()) {
                return array('person');
            }

            return array('company');
        },
    ));
}

validation_groups オプションを使用すると、使用されているデフォルトの検証グループは上書きされます。 エンティティのデフォルトの制約も検証したい時は、次のようにオプションを調整する必要があります。

use AppBundle\Entity\Client;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

// ...
public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function (FormInterface $form) {
            $data = $form->getData();

            if (Client::TYPE_PERSON == $data->getType()) {
                return array('Default', 'person');
            }

            return array('Default', 'company');
        },
    ));
}

バリデーショングループとデフォルト制約がどの様に動くかの詳細は、バリデーショングループを参照してください。

クリックされたボタンに基づくグループ

フォームが複数の送信ボタンを持っている時、フォーム送信に使われたボタンによって、バリデーショングループを変更することができます。 例えば、次に進むか、前に戻ることができるウィザートでのフォームを考えてください。 また、前に戻る時は、フォームのデータは保存されるが検証は行われないことにします。

最初にフォームに2つのボタンを追加する必要があります。

$form = $this->createFormBuilder($task)
    // ...
    ->add('nextStep', SubmitType::class)
    ->add('previousStep', SubmitType::class)
    ->getForm();

その後、前に戻る為のボタンに、特定のバリデーショングループを実行することを設定します。 この例では、検証を行われないようにしたいので、validation_groups オプションに false を設定します。

$form = $this->createFormBuilder($task)
    // ...
    ->add('previousStep', SubmitType::class, array(
        'validation_groups' => false,
    ))
    ->getForm();

これで、フォームは検証制約をスキップします。それでも、まだ、基本的な整合性制約は検証します。 アップロードされたファイルが大きすぎないかや、番号フィールドにテキストを送ってこなかったかといった等。

フィールドタイプ

Symfony は、一般的な入力フィールドとよくあるデータ型の全てをカバーする、多くのフィールドタイプを標準装備しています。

テキストフィールド

選択フィールド

日時フィールド

その他のフィールド

フィールドグループ

非表示フィールド

ボタン

基本フィールド

また、独自のカスタムフィールを作成することもできます。 詳細は、カスタムフィールドタイプの作成方法を参照してください。

フィールドタイプのオプション

各フィールドタイプは、いくつかの設定オプションを持っています。 例えば、dueDate フィールドは現在、3つのセレクトボックスとして表示されています。 しかし、DateType は、1つのテキストボックス(文字列として日付を入力する)として表示されるように設定することができます。

->add('dueDate', DateType::class, array('widget' => 'single_text'))

フォーム

各フィールドタイプはいくつかの異なるオプションを持っています。 これらの多くは、フィールドタイプに固有のものであり、詳細は各タイプのドキュメントに記載されています。

必須オプション

最も一般的なオプションは、どのフィールドにも適用できる必須オプションです。 デフォルトでは、required オプションは true にセットされています。 これは、HTML5 対応のブラウザが、フィールドが空の時、クライアント側で検証を適用することを意味します。 この動作を望まない時には、HTML5 バリデーションを無効にするか、フィールドの require オプションを false にします。

->add('dueDate', 'date', array(
    'widget' => 'single_text',
    'required' => false
))

また、required オプションを設定することは、サーバー側の検証を提供することではない事に注意してください。 つまり、ユーザーが空のフィールドを送信した時(例えば、古いブラウザやウェブサービスによって)、Symfony の NotBlankNotNull 検証制約を使用しない限り、その値は正しい値として受け入れられてしまいます。

つまり、required オプションはナイスですが、サーバー側の検証は常に行う必要があります。

ラベルオプション

フォームフィールドのラベルの為に、label オプションを使うことができます。 これは、どのフィールドにも適用できます。

->add('dueDate', DateType::class, array(
    'widget' => 'single_text',
    'label'  => 'Due Date',
))

また、フィールドのラベルはフォームを表示するテンプレート内でセットすることもできます(この先のセクションでやります)。 入力に関連するラベルが必要ない時は、その値に false を設定することによって、ラベルを無効にすることができます。

フィールドタイプの推測

Task クラスにバリデーション用のメタデータを追加したので、Symfony は既にフィールドについて少し知っています。 もし、あなたが許可するのなら、Symfony はフィールドタイプを「推測」し、それをセットアップすることができます。 この例では、Symfony は、バリデーションルールから、task フィールドが通常の TestType フィールドで、 dueData フィールドが DateType フィールドと推測することができます。

public function newAction()
{
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task')
        ->add('dueDate', null, array('widget' => 'single_text'))
        ->add('save', SubmitType::class)
        ->getForm();
}

add() メソッドの第2引数を省略した時(又は、null を渡した時)、「推測」はアクティブになります。 第3引数にオプションの配列を渡すと(上記の dueDate で行ったように)、それらは推測されたフィールドに適用されます。

フォームが特定の検証グループを使用している時でも、フィールドタイプを推測する時は、すべての検証の制約を考慮します(バリデーショングループで使われていない部分の制約を含めて)。

フィールドタイプオプションの推測

フィールドタイプの推測に加えて、Symfony はまた、いくつかのフィールドオプションの正しい値を推測しようとします。

これらのオプションが設定されている時、フィールドは、HTML5 のクライアント側での検証を提供する、特別な HTML 属性と共に表示されます。 しかし、それはサーバ側の制約と同等の物は生成されません(例えば、Assert\Length)。 手動でサーバー側の検証を追加する必要がありますが、これらのフォールドタイプのオプションは、その情報から推測することができます。

  • required
    • required オプションはバリデーションルールか(例、NotBlandNotNull)、Doctrine のメタデータから(例、nulable)推測されます。クライアント側の検証が自動的に検証ルールにマッチするので、これはとても便利です。
  • max_length
    • フォールドがテキストフィールドの一種である時、max_length オプションはバリデーションルールか(例、LengthRange)、Doctrine のメタデータから(フィールド長を通して)推測されます。

これらのフィールドオプションは、add() メソッドの第2引数が省略された時か、null が渡された時にだけ、推測されます。

推測された値の1つを変更したい時は、オプション配列を渡すことで、それをオーバーライドすることができます。

->add('task', null, array('attr' => array('maxlength' => 4)))

テンプレートでのフォーム表示

これまで、わずか1行のコードでフォーム全体をレンダリングする方法を見てきました。 もちろん、通常は、より柔軟にレンダリングする必要があります。

{# app/Resources/views/default/new.html.twig #}
{{ form_start(form) }}
    {{ form_errors(form) }}

    {{ form_row(form.task) }}
    {{ form_row(form.dueDate) }}
{{ form_end(form) }}
<!-- app/Resources/views/default/newAction.html.php -->
<?php echo $view['form']->start($form) ?>
    <?php echo $view['form']->errors($form) ?>

    <?php echo $view['form']->row($form['task']) ?>
    <?php echo $view['form']->row($form['dueDate']) ?>
<?php echo $view['form']->end($form) ?>

既に、form_start()form_end() 関数は知っています。しかし、他の関数は何をしているのでしょうか?

  • form_errors(form)
    • フォーム全体のグローバルなエラーを表示する(フィールド固有のエラーは各フィールドの次に表示されます)。
  • form_row(form.dueData)
    • 渡されたフォールドの入力項目やラベル、エラーを div タグ(デフォルトでは)の中に表示します。

作業の大部分は form_row ヘルパーによって行われます。 それは、各フォールドの入力項目やラベル、エラーを div タグ(デフォルトでは)の中に表示します。 フォームのテーマのセクションで、form_row の出力をカスタマイズする方法を学びます。

form.vars.value を通して、フォームの現在のデータにアクセスすることができます。

手動での各フィールドの表示

form_row ヘルパーは、フォームの各フィールドを非常に素早く表示できるので、とても便利です(そして、「行」の為に使用されるマークアップをカスタマイズすることもできます)。 しかし、物事はいつも、そのようにシンプルとは限りません。あなたは、手動で各フィールド全体を表示することができます。 次の例の最終的な出力は、form_row ヘルパーを使った時と同じになります。

{{ form_start(form) }}
    {{ form_errors(form) }}

    <div>
        {{ form_label(form.task) }}
        {{ form_errors(form.task) }}
        {{ form_widget(form.task) }}
    </div>

    <div>
        {{ form_label(form.dueDate) }}
        {{ form_errors(form.dueDate) }}
        {{ form_widget(form.dueDate) }}
    </div>

    <div>
        {{ form_widget(form.save) }}
    </div>

{{ form_end(form) }}
<?php echo $view['form']->start($form) ?>

    <?php echo $view['form']->errors($form) ?>

    <div>
        <?php echo $view['form']->label($form['task']) ?>
        <?php echo $view['form']->errors($form['task']) ?>
        <?php echo $view['form']->widget($form['task']) ?>
    </div>

    <div>
        <?php echo $view['form']->label($form['dueDate']) ?>
        <?php echo $view['form']->errors($form['dueDate']) ?>
        <?php echo $view['form']->widget($form['dueDate']) ?>
    </div>

    <div>
        <?php echo $view['form']->widget($form['save']) ?>
    </div>

<?php echo $view['form']->end($form) ?>

自動生成されるラベルが正しくない時は、指定することができます。

{{ form_label(form.task, 'Task Description') }}
<?php echo $view['form']->label($form['task'], 'Task Description') ?>

いくつかのフィールドタイプには追加のオプションがあります。 これらのオプションは各フィールドタイプのドキュメントに記述されていますが、一般的なオプションに attr があります。 それは、フォーム要素の属性を変更することができます。 次の例は、テキストフィールドに、task_field クラスをを追加します。

{{ form_widget(form.task, {'attr': {'class': 'task_field'}}) }}
<?php echo $view['form']->widget($form['task'], array(
    'attr' => array('class' => 'task_field'),
)) ?>

もし、手動でフォームフィールドをレンダリングする必要がある時には、idname, lavel といった、フィールドの個々の値にアクセスすることができます。次の例は、id を取得する例です。

{{ form.task.vars.id }}
<?php echo $form['task']->vars['id']?>

フォームフィールドの name 属性に使用される値を取得するには、full_name 値を使用する必要があります。

{{ form.task.vars.full_name }}
<?php echo $form['task']->vars['full_name'] ?>

Twig テンプレート関数のリファレンス

Twig を使用している場合は、フォーム表示関数のリファレンスマニュアルが提供されています。 ヘルパー関数や各オプションについての詳細は、そちらを参照してください。

アクションとメソッドの変更

ここまでは、form_start() ヘルパーはフォームの開始タグを表示する為に使われてきました。 そして、各フォームは POST リクエストで同じ URL に送信されるものと想定してきました。 時にはこのパラメータを変更したいことがあります。いくつかの異なる方法で、それを行うことができます。 コントローラでフォームを構築する場合、setAction()setMethod() が使えます。

$form = $this->createFormBuilder($task)
    ->setAction($this->generateUrl('target_route'))
    ->setMethod('GET')
    ->add('task', TextType::class)
    ->add('dueDate', DateType::class)
    ->add('save', SubmitType::class)
    ->getForm();

この例では、フォームを処理するコントローラを指す、target_route ルートが作成されていると想定しています。

フォームクラスの作成」では、フォームを構築するコードを独立したクラスに移す方法を学びます。 コントローラで外部のフォームクラスを使用する時、フォームのオプションとして、アクションやメソッドを渡すことができます。

use AppBundle\Form\Type\TaskType;
// ...

$form = $this->createForm(TaskType::class, $task, array(
    'action' => $this->generateUrl('target_route'),
    'method' => 'GET',
));

テンプレートで、form()form_start() に、アクションやメソッドを渡すことで、それらをオーバーライドすることもできます。

{# app/Resources/views/default/new.html.twig #}
{{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }}
<!-- app/Resources/views/default/newAction.html.php -->
<?php echo $view['form']->start($form, array(
    // The path() method was introduced in Symfony 2.8. Prior to 2.8,
    // you had to use generate().
    'action' => $view['router']->path('target_route'),
    'method' => 'GET',
)) ?>

フォームのメソッドが GET でも POST でも無く、PUT か PATCH、DELETE の時、Symfony は _method という名前の hidden フィールドを挿入します。 フォームは通常の POST リクエストで送信されます。しかし Symfony のルータは _method パラメータを検出することができ、PUT や PATCH、DELTE リクエストであることを判断します。 詳細はルートで GET や POST 以外の HTTP メソッドを使う方法を参照してくだい。

フォームクラスの作成

これまで、フォームがコントローラ内で直接作成され、使用されるのを見てきました。 しかし、より良い方法は、個別に分けた PHP クラスを使ってフォームを構築することです。 それは、アプリケーション内での再利用を可能にします。 次に、タスクフォームを構築する為のロジックを収容する新しいクラスを作成します

// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class TaskType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', SubmitType::class)
        ;
    }
}

この新しいクラスは、フォームを作成するのに必要な全ての方法を含んでいます。 これは、コントローラ内で素早くフォームオブジェクトを構築することを可能にします。

// src/AppBundle/Controller/DefaultController.php
use AppBundle\Form\Type\TaskType;

public function newAction()
{
    $task = ...;
    $form = $this->createForm(TaskType::class, $task);

    // ...
}

独自のクラスにフォームのロジックを配置することは、フォームの再利用を簡単にできるようにします。 これは、フォームを作成するベストな方法ですが、最終的な選択はあなた次第です。

全てのフォームは保持する基礎データの名前を知る必要があります(例、AppBundle\Entity\Task)。 通常これは、createForm の第2引数に渡されたオブジェクトに基いて推測されます(例、$task)。 フォームの埋め込みを行う時には、もはやこれは十分ではないでしょう。 いつも必要と言うわけではありませんが、次のように、クラスの型を data_class オプションに明示的に指定することは、全般的によいアイデアです。

use Symfony\Component\OptionsResolver\OptionsResolver;

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'AppBundle\Entity\Task',
    ));
}

フォームをオブジェクトにマッピングする時、全てのフィールドがマッピングされます。 マッピングされたオブジェクトに存在しないフォームのフィールドは例外の発生を引き起こします。

基礎オブジェクトにマッピングできないフォームの追加フィールドが必要な時には(例、「規約に同意しますか?」チェックボックス等)、mapped オプションに false をセットする必要があります。

use Symfony\Component\Form\FormBuilderInterface;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('task')
        ->add('dueDate', null, array('mapped' => false))
        ->add('save', SubmitType::class)
    ;
}

さらに、送信データに含めないフォームの項目がある時には、これらのフィールドに明示的に null をセットします。

コントローラ内では、次のようにフォールドデータにアスセスできます。

$form->get('dueDate')->getData();

また、マップされていないフィールドのデータを直接変更することもできます。

$form->get('dueDate')->setData(new \DateTime());

サービスとしてのフォームの定義

フォームは、いくつかの外部依存を持つ必要があるかも知れません。 そのような時はフォームをサービスとして定義して、必要な全ての依存を注入することができます。

サービスとサービスコンテナは、このブックの後半で解説します。 その章を読み終えると、より内容が明確に分かります。

フォーム内で app.my_service と定義したサービスを使いたい時は、コンストラクターを作成してサービスを受け取ります。

// src/AppBundle/Form/Type/TaskType.php
namespace AppBundle\Form\Type;

use App\Utility\MyService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;

class TaskType extends AbstractType
{
    private $myService;

    public function __construct(MyService $myService)
    {
        $this->myService = $myService;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // You can now use myService.
        $builder
            ->add('task')
            ->add('dueDate', null, array('widget' => 'single_text'))
            ->add('save', SubmitType::class)
        ;
    }
}

そして、フォームをサービスとして定義します。

# src/AppBundle/Resources/config/services.yml
services:
    app.form.type.task:
        class: AppBundle\Form\Type\TaskType
        arguments: ["@app.my_service"]
        tags:
            - { name: form.type }
<!-- src/AppBundle/Resources/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.form.type.task" class="AppBundle\Form\Type\TaskType">
            <tag name="form.type" />
            <argument type="service" id="app.my_service"></argument>
        </service>
    </services>
</container>
// src/AppBundle/Resources/config/services.php
use Symfony\Component\DependencyInjection\Reference;

$container->register('app.form.type.task', 'AppBundle\Form\Type\TaskType')
    ->addArgument(new Reference('app.my_service'))
    ->addTag('form.type')

詳細はサービスとしてのフィールドタイプの作成を参照してください。

フォームと Doctrine

フォームの目的はオブジェクト(例、Task)のデータを HTML のフォームに変換することと、フォームから送信されたデータを元のオブジェクトに変換することです。 このように、データベースへの Task オブジェクトの保存に関しては、フォームとは完全に無関係です。 しかし、Task クラスが Doctrine を経由して保存できるように設定されているのであれば(オブジェクトのクラスにマッピング情報が追加されていれば)、フォームから送信されたデータを検証して、保存することができます。

if ($form->isValid()) {
    $em = $this->getDoctrine()->getManager();
    $em->persist($task);
    $em->flush();

    return $this->redirectToRoute('task_success');
}

もし何らかの理由により、オリジナルの $task オブジェクトへの参照を持っていない場合は、次のようにフォームから取得することができます。

$task = $form->getData();

詳細はDoctrine ORM の章を参照してください。

これを理解するのに重要なことは、フォームが送信された時、送信されたデータは直ちにフォームの基礎オブジェクトに変換されるということです。 そのデータを保存したい時は、シンプルにそのオブジェクト自体(既に送信されたデータが含まれている)を保存します。

埋め込みフォーム

複数のオブジェクトのフォールドを含むフォームを作りたいことがよくあります。 例えば、登録フォームは User オブジェクトだけでなく、Address オブジェクトに属するデータを持つこともできます。 幸運なことに、これはフォームコンポーネントで簡単に、自然に行うことができます。

単一オブジェクトの埋め込み

Task がシンプルな Category オブジェクトに属しているとします。Category クラスを作成することから始めます。

// src/AppBundle/Entity/Category.php
namespace AppBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;

class Category
{
    /**
     * @Assert\NotBlank()
     */
    public $name;
}

次に、Task クラスに新しい category プロパティを追加します。

// ...

class Task
{
    // ...

    /**
     * @Assert\Type(type="AppBundle\Entity\Category")
     * @Assert\Valid()
     */
    protected $category;

    // ...

    public function getCategory()
    {
        return $this->category;
    }

    public function setCategory(Category $category = null)
    {
        $this->category = $category;
    }
}

Valid 制約が category プロパティに追加されました。 これは、エンティティに対する検証を実行します。 もし、この制約を省略すると、子エントリ(Category)の検証は行われません。

これらをアプリケーションへ反映したら、Category オブジェクトをユーザーが修正できるようにフォームクラスを作成します。

// src/AppBundle/Form/Type/CategoryType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CategoryType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('name');
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Category',
        ));
    }
}

ここでの最終目標は Task が持つ CategoryTask のフォームから変更できるようにすることです。 それをする為に TaskTypecategory フィールドを追加し、そこに CategoryType クラスを設定します。

use Symfony\Component\Form\FormBuilderInterface;
use AppBundle\Form\Type\CategoryType;

public function buildForm(FormBuilderInterface $builder, array $options)
{
    // ...

    $builder->add('category', CategoryType::class);
}

これで CategoryTypeTaskType と共に表示できるようになりました。 オリジナルの Task フィールドと同じ方法で、Category フィールドを表示します。

{# ... #}

<h3>Category</h3>
<div class="category">
    {{ form_row(form.category.name) }}
</div>

{# ... #}
<!-- ... -->

<h3>Category</h3>
<div class="category">
    <?php echo $view['form']->row($form['category']['name']) ?>
</div>

<!-- ... -->

フォームが送信された時、Category フィールドに対応するデータが Category のインスタンスを構築する為に使用されます。 そして、それらは、Task インスタンスの category フィールドにセットされます。

Category インスタンスは $task->getCategory() を通して、自然にアクセスでき、データベースに保存したり、必要に応じて使用することができます。

フォームコレクションの埋め込み

また、1つのフォームの中に、フォームのコレクションを埋め込む事もできます(例、複数の Task サブフォームを持つ Category フォーム)。 フィールドタイプに collection を使うことで、これを行えます。

詳細は、クックブックのフォームのコレクションを埋め込む方法CollectionTypeリファレンスを参照してください。

フォームのテーマ

フォームの表示は全てカスタマイズすることが出来ます。フォームの各行の表示方法や、エラー表示に使うマークアップ、textarea タグがどの様にレンダリングされるかさえも変更することは自由です。制限はありません。そして、個々のカスタマイズは別々の場所で使用することができます。

Symfony は label タグや input タグ、エラーメッセージのようなフォームの全てのパーツを表示する為に、テンプレートを使用しています。

Twig では、各フォームのフラグメント(部品)は Twig の block で書かれています。フォームの特定のパーツをカスタマイズするには、対応する block をオーバーライドする必要があります。

PHP では、各フォームのフラグメント(部品)は個々のテンプレートファイルとして書かれています。フォームの特定のパーツをカスタマイズするには、新しいファイルを作成して、既存のテンプレートファイルをオーバーライドする必要があります。

これがどの様に動くのかを理解するために、form_row フラグメントをカスタマイズして、各行を囲む div タグに class 属性を追加してみます。 これを行うには、新しいマークアップを保存するテンプレートファイルを作成します。

{# app/Resources/views/form/fields.html.twig #}
{% block form_row %}
{% spaceless %}
    <div class="form_row">
        {{ form_label(form) }}
        {{ form_errors(form) }}
        {{ form_widget(form) }}
    </div>
{% endspaceless %}
{% endblock form_row %}
<!-- app/Resources/views/form/form_row.html.php -->
<div class="form_row">
    <?php echo $view['form']->label($form, $label) ?>
    <?php echo $view['form']->errors($form) ?>
    <?php echo $view['form']->widget($form, $parameters) ?>
</div>

form_row フォームフラグメントは form_row 関数を使ってフィールドを表示する時に使用されます。 上記で定義した新しい form_row フラグメントを使うことをフォームコンポーネントに指示する為に、フォームを表示するテンプレートの上部に以下のような記述を追加します。

{# app/Resources/views/default/new.html.twig #}
{% form_theme form 'form/fields.html.twig' %}

{# or if you want to use multiple themes #}
{% form_theme form 'form/fields.html.twig' 'form/fields2.html.twig' %}

{# ... render the form #}
<!-- app/Resources/views/default/new.html.php -->
<?php $view['form']->setTheme($form, array('form')) ?>

<!-- or if you want to use multiple themes -->
<?php $view['form']->setTheme($form, array('form', 'form2')) ?>

<!-- ... render the form -->

form_theme タグ(Twigの場合)は指定されたテンプレートで定義されたフラグメントをインポートします。そして、フォームを表示する時に、それらを使います。 つまり、このテンプレートで form_row 関数が呼ばれた時に、あなたのカスタムテーマの form_row block を使用します(Symfony 付属のデフォルト form_row の代わりに)。

カスタムテーマは全ての block をオーバーライドする必要はありません。 カスタムテーマでオーバーライドしていない block を表示する時は、テーマエンジンはグローバルテーマ(バンドルで定義された)を表示します。

特定のカスタムテーマが提供されている場合、それらはグローバルテーマを検索する前に、記載された順に検索されます。

フォームのパーツをカスタマイズする為には、適切なフラグメントをオーバライドするだけです。 オーバーライドできる block 名やファイル名は、次のセクションで説明します。

詳細は、フォーム表示のカスタマイズ方法を参照してください。

フォームフラグメント名

HTML のフォームやエラー、ラベル等の全てのパーツは、複数の Twig block や 複数の PHP テンプレートファイルとしてベーステーマに定義されています。

Twig の場合、全てのブロックは Twig Bridge の中の1つのテンプレートファイル内に定義されています。 このファイル内で、フォームを表示するのに必要な全てのブロックと全てのデフォルトタイプを見ることができます。

PHP の場合、フラグメントは個々のテンプレートファイルです。デフォルトではそれらは、FrameworkBundle の Resources/views/Form ディレクトリに置かれています(GitHub で表示)。

各フラグメント名は基本的なパターンに従っていて、アンダースコア(_)で区切られた2つの部分に別れています。

  • form_row - ほとんどの入力フィールドを表示する為に、form_row 関数によって使用されます。
  • textarea_widget - textarea フィールドを表示する為に、form_widget 関数によって使用されます。
  • form_errors - フィールドのエラーを表示する為に、form_errors 関数によって使用されます。

各フラグメント名は type_part パターンに従っています。type 部分は表示されるフィールドの型(例、textarea, checkbox, date, etc)に対応しています。 part 部分は何を表示するかに対応しています(例、label, widget, errors, etc)。 デフォルでは、4つの part があります。

part 内容
label form_label フォールドのラベルを表示する
widgetform_widgetフォールドの HTML を表示する
errorsform_errorsフォールドのエラーを表示する
row form_row フォールドの行全体を表示する(label, widget & errors)

実際には他に、rowsrest の2つのパーツがあります。しかし、これらをオーバーライドする心配はほどんどありません。

フィールドタイプとカスタマイズしたいパーツを知ることで、オーバライドする必要のあるフラグメント名を特定できます。

テンプレートフラグメントの継承

いくつかのケースでは、カスタマイズするフラグメントが欠落しているように見えることがあります。 例えば、Symfony によって提供されるデフォルトのテーマには textarea_errors フラグメントはありません。 では、textarea のエラーはどの様に表示するのでしょう?

その答えは、form_errors フラグメントです。 Symfony は textarea のエラーを表示する時、form_errors フラグメントを使用する前に、textarea_errors ブラグメントを探します。 各フィールドタイプは親タイプを持っています(textarea の親タイプは text です。text の親タイプは form です)。 そして、Symfony はフラグメントが存在しない時、親タイプのフラグメントを使用します。

なので、textarea フィールドのエラーをオーバーライドする為には、form_errors フラグメントをコピーして、textarea_errors に名前を変更します。そして、それをカスタマイズします。全てのフィールドのデフォルトのエラー表示をオーバーライドするには、form_errors フラグメントをコピーしてカスタマイズします。

各フォールドの親タイプは、フォームタイプレファレンスで知ることができます。

グローバルフォームテーマ

上記の例では、1つのフォームに対してカスタムフォームフラグメントをインポートする為に、form_theme(Twig の場合)ヘルパーを使用しましたが、 プロジェクト全体で使うフォームカスタマイズをインポートする事を Symfony に指示することもできます。

Twig

フォームブロックをカスタマイズした fields.html.twig テンプレートを全てのテンプレートに自動的に含めるには、アプリケーションの設定ファイルを修正します。

# app/config/config.yml
twig:
    form_themes:
        - 'form/fields.html.twig'
    # ...
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:twig="http://symfony.com/schema/dic/twig"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/twig http://symfony.com/schema/dic/twig/twig-1.0.xsd">

    <twig:config>
        <twig:theme>form/fields.html.twig</twig:theme>
        <!-- ... -->
    </twig:config>
</container>
// app/config/config.php
$container->loadFromExtension('twig', array(
    'form_themes' => array(
        'form/fields.html.twig',
    ),
    // ...
));

これで、fields.html.twig テンプレート内の全てのブロックは、フォームの出力定義でグローバルに使用されます。

1つのファイルで全てのフォーム出力をカスタマイズする(Twig の場合)

Twig では、カスタマイズが必要なテンプレートの中だけで form block をカスタマイズすることもできます。

{% extends 'base.html.twig' %}

{# import "_self" as the form theme #}
{% form_theme form _self %}

{# make the form fragment customization #}
{% block form_row %}
    {# custom field row output #}
{% endblock form_row %}

{% block content %}
    {# ... #}

    {{ form_row(form.task) }}
{% endblock %}

{% form_theme form _self %} タグはテンプレートの中で直接 form block をカスタマイズすることを可能にします。 この方法を使うと、1つのテンプレートでしか必要のないフォームのカスタマイズを素早く作成できます。

{% form_theme form _self %} 機能はテンプレートが他のテンプレートを拡張している時だけ機能します。 もし、そうでないなら、form_theme で個別のテンプレートを指定する必要があります。

PHP

app/Resources/views/Form ディレクトリに作成したカスタマイズテンプレートを全てのテンプレートに自動的に含めるには、アプリケーションの設定ファイルを修正します。

# app/config/config.yml
framework:
    templating:
        form:
            resources:
                - 'Form'
# ...
<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:framework="http://symfony.com/schema/dic/symfony"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
        http://symfony.com/schema/dic/symfony http://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

    <framework:config>
        <framework:templating>
            <framework:form>
                <framework:resource>Form</framework:resource>
            </framework:form>
        </framework:templating>
        <!-- ... -->
    </framework:config>
</container>
// app/config/config.php
$container->loadFromExtension('framework', array(
    'templating' => array(
        'form' => array(
            'resources' => array(
                'Form',
            ),
        ),
    ),
    // ...
));

これで、app/Resources/views/Form ディレクトリ内の全てのフラグメントはフォームの出力定義でグローバルに使用されます。

CSRF 保護

CSRF(Cross-site request forgery)は、悪意のあるユーザーが正当なユーザーが知らないうちに、意図していないデータの送信を行う手法です。 幸い、CSRF 攻撃は フォームで CSRF トークンを使用することで防ぐことができます。

嬉しいことに、Symfony はデフォルトで自動的に CSRF トークンの埋め込みと検証を行います。 これは、何もしないでも、CSRF 保護ができていることを意味します。 実際、この章の全てのフォームは CSRF 保護がされています。

CSRF 保護はフォームに _token と呼ばれる hidden フィールドを追加することによって動きます。 どれには、あたなとあなたのユーザーしか知らない値が含まれます。 これは、ユーザーが与えられたデータを送信していることを保証します。 Symfony は自動的にこのトークンの存在と正しいものかを検証します。

_token hidden フィールドはテンプレート内で form_end() 関数が使用された時に、自動的にレンダリングされます。 form_end() はレンダリングされていない全てのフィールドを出力することを保証します。

トークンはセッションに保存されるので、CSRF 保護を備えたフォームをレンダリングするとすぐにセッションが開始されます。

CSRF トークンはフォームごとにカスタマイズすることができます。例えば

use Symfony\Component\OptionsResolver\OptionsResolver;

class TaskType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class'      => 'AppBundle\Entity\Task',
            'csrf_protection' => true,
            'csrf_field_name' => '_token',
            // a unique key to help generate the secret token
            'csrf_token_id'   => 'task_item',
        ));
    }

    // ...
}

CSRF 保護を無効にするには、csrf_protection オプションに false をセットします。 また、カスタマイズはプロジェクト全体に行うこともできます。 詳細はフォーム設定リファレンスを参照してください。

csrf_token_id オプションの指定は任意ですが、フォームごとに違った物を作成することによって、トークン生成のセキュリティーが大幅に強化されます。

CSRFトークンは、ユーザ毎に異なるようになっています。 この種の保護を含むフォームを持つページをキャッシュしようとする時、警戒する必要があるのはこの為です。 詳細はCSRF 保護をしたフォームを含むページのキャッシュを参照してください。

クラスなしのフォーム

多くの場合、フォームはオブジェクトと結びついています。そして、フォームのフィールドはそのオブジェクトのプロパティに対してデータの取得や保存を行います。 これはまさに、これまでこの章で Task クラスに対して見てきた通りです。

しかし時々、クラス無しでフォームを使いたい、送信されたデータを配列にしたいことがあります。 これは実際、簡単にできます。

// make sure you've imported the Request namespace above the class
use Symfony\Component\HttpFoundation\Request;
// ...

public function contactAction(Request $request)
{
    $defaultData = array('message' => 'Type your message here');
    $form = $this->createFormBuilder($defaultData)
        ->add('name', TextType::class)
        ->add('email', EmailType::class)
        ->add('message', TextareaType::class)
        ->add('send', SubmitType::class)
        ->getForm();

    $form->handleRequest($request);

    if ($form->isValid()) {
        // data is an array with "name", "email", and "message" keys
        $data = $form->getData();
    }

    // ... render the form
}

実際のところデフォルトでは、フォームはオブジェクトの代わりにデータの配列で動くようになっています。 この動きを変更して代わりにオブジェクトをフォームに結びつける方法は2つあります。

  1. フォームを作成する時にオブジェクトを渡す(createFormBuilder の最初の引数か、createForm の第2引数として)。
  2. フォームの data_class オプションを表記する。

これらの何れも実施しない場合、フォームは配列としてデータを返します。 この例では、$defaultData はオブジェクトではないので(そして、data_class オプションもセットしていないので)、$form->getData() は最終的に配列を返します。

リクエストオブジェクトを通して、直接 POST の値にアクセスすることもできます。

$request->request->get('name');

しかし、多くの場合、getData() メソッドを使うことが良い選択です。 それは、フォームコンポーネントによって、データを変換した後にデータを返します(通常はオブジェクト)。

バリデーションの追加

唯一不足しているのがバリデーションです。通常、$form->isValid() を実行した時に、オブジェクトはクラスに適用した制約を読み取ることによって検証されます。 フォームがオブジェクトにマップされている時は、これがほとんどの場合に使用するアプローチです。詳細はバリデーションを参照してください。

しかし、フォームをオブジェクトにマップせずに、送信したデータをシンプルな配列として取得したい時は、フォームのデータにどの様に制約を追加するのでしょうか?

その答えは、あなた自身で制約をセットアップすることです。そして、それらを個々のフィールドに追加します。 全体的なアプローチはバリデーションの章でもう少し詳しく説明されていますが、ここでも簡単な例を示します。

use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Form\Extension\Core\Type\TextType;

$builder
   ->add('firstName', TextType::class, array(
       'constraints' => new Length(array('min' => 3)),
   ))
   ->add('lastName', TextType::class, array(
       'constraints' => array(
           new NotBlank(),
           new Length(array('min' => 3)),
       ),
   ))
;

もし、バリデーショングループを使用する場合は、フォームを作成する時に、Default グループを参照するか、追加している制約に正しいグループを設定する必要があります。

new NotBlank(array('groups' => array('create', 'update'))

まとめ

今やあなたは、アプリケーションの為に複雑で機能的なフォームを作成する為に必要な構成要素を全て知っています。 フォームを作成する時、フォームの最初の目的は、ユーザーがデータを変更出来るように、オブジェクトを HTML のフォームに変換することであるということを忘れないでください。 2つ目の目的は、ユーザーによって送信されたデータを取得し、オブジェクトに再度当てはめることです。

フォームの強力な世界を学ぶことはもっとあります。例えば、Doctrine でファイルのアップロードを扱う方法や、動的にサブフォームを追加できるフォームの作り方等です(例えば、送信前に JavaScript を使ってフィールドを追加できる TODO リスト等)。 これらの内容はクックブックを参照してください。 また、ぜひフィールドタイプリファレンスも学んで下さい。それには、各フィールドタイプやそのオプションの使い方の例が記載されています。

クックブックの参照先