【问题标题】:Symfony2 Forms and Polymorphic collectionsSymfony2 表单和多态集合
【发布时间】:2012-07-16 06:34:16
【问题描述】:

我在玩 Symfony2 并且我有点不确定 Symfony2 如何处理 View 组件中的多态集合。似乎我可以创建一个包含 AbstractChildren 集合的实体,但不确定如何在 Form Type 类中使用它。

例如,我有以下实体关系。

/**
 * @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()));
    }
}

我不确定如何处理这种情况,您实际上需要为集合中的每个项目类别使用不同的表单类型?

【问题讨论】:

    标签: symfony doctrine polymorphism


    【解决方案1】:

    我最近解决了一个类似的问题 - Symfony 本身对多态集合没有做出任何让步,但是使用 EventListener 来扩展表单很容易为它们提供支持。

    下面是我的 EventListener 的内容,它使用了与 Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener 类似的方法,提供集合表单类型的正常功能的事件侦听器:

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

    【讨论】:

    • 你知道如何让它与 Symfony2 一起工作吗?具体来说,“$child->getConfig()->getOptions();”在 2.0 中不可用,因此我无法获得表单的原始选项。如果我忽略选项,我最终会得到“属性“0”或方法“get0()”或方法“is0()”都存在于类“Doctrine\ORM\PersistentCollection”中
    • @CriticalImpact 我查看了 2.0 Form 组件的源代码,但看不到任何方法可以真正达到相同的效果(这些选项不会长期存储-术语)。但是,如果您可以始终使用默认选项,则可以做到这一点-要解决您遇到的错误,您应该只需要适当地设置 property_path (不幸的是,我不得不把它留给您发现 2.0 对集合中的属性路径使用什么格式 - 不过,一些放置得当的 var_dump 应该可以解决问题)
    • 我实际上设法想出了另一个解决方案。我为 FormEvents::PRE_SET_DATA 添加了一个事件监听器,得到了支持对象(在我的例子中是一个问题对象),确定了问题的类型(我在我的问题中设置了一些东西,说明它是否是一个复选框,是/否、文本字段等),然后根据问题对象中设置的类型将该字段添加到表单中。
    【解决方案2】:

    在您给出的示例中,您必须为 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
        }
    }
    

    现在这将两个表单 SubsciptionOrderType,ProductOrderType 添加到主表单 OrderType 。因此,稍后在控制器中,如果您初始化此表单,您将获得订阅和产品表单的所有字段以及 OrderType 的字段。

    如果仍然不清楚,我希望这能回答您的问题,请在此处查看嵌入多个表单的文档。 http://symfony.com/doc/current/cookbook/form/form_collections.html

    【讨论】:

    • 从这个解决方案中,我不能只拥有一个产品和/或一个订阅吗?我更喜欢拥有一个可以是产品或订阅的对象集合,并让 Symfony 决定哪种表单类型适合集合中的实体
    猜你喜欢
    • 2014-07-07
    • 1970-01-01
    • 2013-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-04
    相关资源
    最近更新 更多