Symfony2で複雑なフォームの実装について

Symfony2で少し複雑なフォームを作る際のメモ書き。

例として1つの商品は複数のカテゴリに属する。
要件としては管理画面の商品の設定画面にて、複数のカテゴリを設定することとする。
更に設定したカテゴリにはついでに何かメモ書きを設定出来ることとする。

各Entityは以下のように実装する。

Product.php (商品)

<?php
// ...
class Product
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="product_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string $productName
     *
     * @ORM\Column(name="product_name", type="string", length="255", nullable=true)
     */
    private $productName;

    /**
     * @var string $amount
     *
     * @ORM\Column(name="amount", type="integer", nullable=false)
     */
    private $amount;

    /**
     * @var ArrayCollection $productCategories
     *
     * @ORM\OneToMany(targetEntity="ProductCategory", mappedBy="product", cascade={"persist", "remove"})
     */
    private $productCategories;


    public function __construct()
    {
        $this->productCategories = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function getId()
    {
        return $this->id;
    }

    public function setProductName($productName)
    {
        $this->productName = $productName;
    }

    public function getProductName()
    {
        return $this->productName;
    }

    public function setAmount($amount)
    {
        $this->amount = $amount;
    }

    public function getAmount()
    {
        return $this->amount;
    }

    public function addProductCategory(Category $category)
    {
        $this->productCategories[] = $category;
    }

    public function getProductCategories()
    {
        return $this->productCategories;
    }
}

Category.php (カテゴリ)

<?php
// ...
class Category
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="category_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string $categoryName
     *
     * @ORM\Column(name="category_name", type="string", length="100", nullable=true)
     */
    private $categoryName;


    public function __toString()
    {
        return $this->getCategoryName();
    }

    public function getId()
    {
        return $this->id;
    }

    public function setCategoryName($categoryName)
    {
        $this->categoryName = $categoryName;
    }

    public function getCategoryName()
    {
        return $this->categoryName;
    }
}

ProductCategory.php (商品・カテゴリのリレーション)

<?php
// ...
class ProductCategory
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="product_category_id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var Product $product
     *
     * @ORM\OneToOne(targetEntity="Product")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id")
     */
    private $product;

    /**
     * @var Category $category;
     *
     * @ORM\OneToOne(targetEntity="Category")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;

    /**
     * @var string $memo
     *
     * @ORM\Column(name="memo", type="string", nullable=true)
     */
    private $memo;


    public function getId()
    {
        return $this->id;
    }

    public function setProduct(Product $product)
    {
        $this->product = $product;
    }

    public function getProduct()
    {
        return $this->product;
    }

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

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

    public function setMemo($memo)
    {
        $this->memo = $memo;
    }

    public function getMemo()
    {
        return $this->memo;
    }
}

で、次に商品登録用のフォームを作成する。
下記がその実装。

ProductType.php

<?php
namespace Sample\Bundle\DemoBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

use Sample\Bundle\DemoBundle\Form\Type\ProductCategoryType;

class ProductType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('productName')
            ->add('amount')
            ->add('productCategories', 'collection', array(
                'type'         => new ProductCategoryType(),
                'allow_add'    => true,
                'allow_delete' => true,
                'prototype'    => true,
            ))
        ;
    }

    public function getName()
    {
        return 'product';
    }
}

呼び出しているProductCategoryTypeクラスは下記のとおり。

ProductCategoryType.php

<?php
namespace Sample\Bundle\DemoBundle\Form\Type\Edit;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ProductCategoryType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
            ->add('category', 'entity', array(
                'empty_value' => '選択してください',
                'class'       => 'Sample\\Bundle\\DemoBundle\\Entity\\Category',
            ))
            ->add('memo')
        ;
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'Sample\Bundle\DemoBundle\Entity\ProductCategory',
        );
    }

    public function getName()
    {
        return 'productCategory';
    }
}

ここまで実装できたら後は普通にProductTypeを呼び出してフォームを作成するだけ。
Twig内では下記のようにしてProductCategoryのフォームを呼び出すことができる。

{# プロトタイプを取得 #}
{{ form_widget(form.productCategories.get('prototype').getChild('category')) }}
{{ form_widget(form.productCategories.get('prototype').getChild('memo')) }}

{# bindされているものを取り出す #}
{% for productCategory in form.productCategories %}
    {{ form_widget(productCategory.category) }}
    {{ form_widget(productCategory.memo) }}
{% endfor %}

後は自分でJavascriptの実装をするなりして複雑なフォームを実装していけた。