12

私は Symfony2 で遊んでいますが、Symfony2 が View コンポーネントで多態的なコレクションをどのように処理するかはわかりません。AbstractChildren のコレクションを使用してエンティティを作成できるようですが、フォーム タイプ クラス内でそれをどうするかはわかりません。

たとえば、次のエンティティ関係があります。

/**
 * @ORM\Entity
 */
class Order
{
    /**
     * @ORM\OneToMany(targetEntity="AbstractOrderItem", mappedBy="order", cascade={"all"}, orphanRemoval=true)
     * 
     * @var AbstractOrderItem $items;
     */
    $orderItems;  
    ...
}


/**
 * Base class for order items to be added to an Order
 *
 * @ORM\Entity
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="discr", type="string")
 * @ORM\DiscriminatorMap({
 *     "ProductOrderItem" = "ProductOrderItem",
 *     "SubscriptionOrderItem " = "SubscriptionOrderItem "
 * })
 */
class AbstractOrderItem
{
    $id;
    ...
}

/**
 * @ORM\Entity
 */
class ProductOrderItem  extends AbstractOrderItem
{
    $productName;
}

/**
 * @ORM\Entity
 */
class SubscriptionOrderItem extends AbstractOrderItem
{
    $duration;
    $startDate;
    ...
}

十分に単純ですが、注文クラスのフォームを作成するとき

class OrderType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('items', 'collection', array('type' => AbstractOrderItemType()));
    }
}

コレクション内のアイテムのクラスごとに異なるフォーム タイプが効果的に必要なこの状況を処理する方法がわかりません。

4

2 に答える 2

9

私は最近、同様の問題に取り組みました。Symfony 自体はポリモーフィック コレクションに対して譲歩しませんが、EventListener を使用してフォームを拡張することで、それらのサポートを簡単に提供できます。

以下は、Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener (コレクション フォーム タイプの通常の機能を提供するイベント リスナー) と同様のアプローチを使用する、私の EventListener の内容です。

namespace Acme\VariedCollectionBundle\EventListener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;

class VariedCollectionSubscriber implements EventSubscriberInterface
{
    protected $factory;
    protected $type;
    protected $typeCb;
    protected $options;

    public function __construct(FormFactoryInterface $factory, $type, $typeCb)
    {
        $this->factory = $factory;
        $this->type = $type;
        $this->typeCb = $typeCb;
    }

    public static function getSubscribedEvents()
    {
        return array(
            FormEvents::PRE_SET_DATA => 'fixChildTypes'
        );
    }

    public function fixChildTypes(FormEvent $event)
    {
        $form = $event->getForm();
        $data = $event->getData();

        // Go with defaults if we have no data
        if($data === null || '' === $data)
        {
            return;
        }

        // It's possible to use array access/addChild, but it's not a part of the interface
        // Instead, we have to remove all children and re-add them to maintain the order
        $toAdd = array();
        foreach($form as $name => $child)
        {
            // Store our own copy of the original form order, in case any are missing from the data
            $toAdd[$name] = $child->getConfig()->getOptions();
            $form->remove($name);
        }
        // Now that the form is empty, build it up again
        foreach($toAdd as $name => $origOptions)
        {
            // Decide whether to use the default form type or some extension
            $datum = $data[$name] ?: null;
            $type = $this->type;
            if($datum)
            {
                $calculatedType = call_user_func($this->typeCb, $datum);
                if($calculatedType)
                {
                    $type = $calculatedType;
                }
            }
            // And recreate the form field
            $form->add($this->factory->createNamed($name, $type, null, $origOptions));
        }
    }
}

このアプローチを使用することの欠点は、送信時にポリモーフィック エンティティのタイプを認識するために、バインドする前に関連するエンティティを使用してフォームにデータを設定する必要があることです。そうしないと、リスナーはデータの実際のタイプを確認する方法がありません。は。FormTypeGuesser システムを使用してこれを回避できる可能性がありますが、それは私のソリューションの範囲を超えていました。

同様に、このシステムを使用するコレクションは引き続き行の追加/削除をサポートしますが、すべての新しい行が基本型であると想定します - それらを拡張エンティティとして設定しようとすると、含まれているフォームに関するエラーが発生します追加のフィールド。

簡単にするために、便利な型を使用してこの機能をカプセル化します。その例については以下を参照してください。

namespace Acme\VariedCollectionBundle\Form\Type;

use Acme\VariedCollectionBundle\EventListener\VariedCollectionSubscriber;
use JMS\DiExtraBundle\Annotation\FormType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;

/**
 * @FormType()
 */
class VariedCollectionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // Tack on our event subscriber
        $builder->addEventSubscriber(new VariedCollectionSubscriber($builder->getFormFactory(), $options['type'], $options['type_cb']));
    }

    public function getParent()
    {
        return "collection";
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setRequired(array('type_cb'));
    }

    public function getName()
    {
        return "varied_collection";
    }
}

例: 名前空間 Acme\VariedCollectionBundle\Form;

use Acme\VariedCollectionBundle\Entity\TestModelWithDate;
use Acme\VariedCollectionBundle\Entity\TestModelWithInt;
use JMS\DiExtraBundle\Annotation\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\AbstractType;

/**
 * @FormType()
 */
class TestForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $typeCb = function($datum) {
            if($datum instanceof TestModelWithInt)
            {
                return "test_with_int_type";
            }
            elseif($datum instanceof TestModelWithDate)
            {
                return "test_with_date_type";
            }
            else
            {
                return null; // Returning null tells the varied collection to use the default type - can be omitted, but included here for clarity
            }
        };

        $builder->add('demoCollection', 'varied_collection', array('type_cb' => $typeCb,  /* Used for determining the per-item type */
                                                                   'type' => 'test_type', /* Used as a fallback and for prototypes */
                                                                   'allow_add' => true,
                                                                   'allow_remove' => true));
    }

    public function getName()
    {
        return "test_form";
    }
}
于 2012-07-23T11:02:00.960 に答える
-1

あなたが与えた例では、それらの ProductOrder と SubscriptionOrder に対して異なるフォーム クラスを作成する必要があります。

class ProductOrderType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        //Form elements related to Product Order here
    }
}

class SubsciptionOrderType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        //Form elements related SubscriptionOrder here
    }
}

OrderType フォーム クラスに、次のように両方のフォームを追加します。

class OrderType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
         $builder->add('product',new ProductOrderType())
         $builder->add('subscription',new SubsciptionOrderType())
        //Form elements related to order here
    }
}

これで、2 つのフォーム SubsciptionOrderType,ProductOrderType がメイン フォーム OrderType に追加されます。したがって、後でコントローラーでこのフォームを初期化すると、サブスクリプションおよび製品フォームのすべてのフィールドが OrderType のフィールドとともに取得されます。

それでも不明な場合は、ここで複数のフォームを埋め込むためのドキュメントを参照してください。http://symfony.com/doc/current/cookbook/form/form_collections.html

于 2012-07-23T09:48:27.943 に答える