2012-07-16 39 views
11

Estoy jugando con Symfony2 y no estoy seguro de cómo Symfony2 maneja las colecciones polimórficas en el componente Ver. Parece que puedo crear una entidad con una colección de AbstractChildren, pero no estoy seguro de qué hacer con ella dentro de una clase de tipo de formulario.Symfony2 Formas y colecciones polimórficas

Por ejemplo, tengo la siguiente relación de entidad.

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

bastante simple, pero cuando estoy crean una forma para mi clase de orden

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

No estoy seguro de cómo manejar esta situación en la que necesita efectivamente un diferente tipo de formulario para cada clase de elemento de la ¿colección?

Respuesta

9

Hace poco abordé un problema similar - Symfony en sí mismo no hace concesiones para las colecciones polimórficas, pero es fácil proporcionar soporte para ellas utilizando un EventListener para extender el formulario.

A continuación se muestra el contenido de mi EventListener, que utiliza un enfoque similar al de Symfony \ Component \ Form \ Extension \ Core \ EventListener \ ResizeFormListener, el detector de eventos que proporciona una funcionalidad normal de la colección de la forma del tipo:

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

La desventaja de utilizar este enfoque es que para reconocer los tipos de sus entidades polimórficas en el envío, debe establecer los datos en su formulario con las entidades pertinentes antes de vincularlo, de lo contrario, el oyente no tiene manera de determinar qué tipo los datos realmente son Podría trabajar con el sistema FormTypeGuesser, pero eso estaba más allá del alcance de mi solución.

De forma similar, aunque una colección que utiliza este sistema sigue admitiendo agregar/eliminar filas, supondrá que todas las filas nuevas son del tipo base; si intenta configurarlas como entidades extendidas, le dará un error sobre el formulario que contiene campos adicionales.

Por razones de simplicidad, utilizo un tipo de conveniencia para encapsular esta funcionalidad - ver abajo para eso y un ejemplo:

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

Ejemplo: espacio de nombres 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"; 
    } 
} 
+0

¿Tiene alguna idea de cómo hacer que esto funcione con Symfony2? Específicamente, "$ child-> getConfig() -> getOptions();" no está disponible en 2.0 y, como tal, no puedo obtener las opciones originales para el formulario. Si dejo las opciones eventualmente obtengo "Ni propiedad" 0 "ni el método" get0() "ni el método" is0() "existe en la clase" Doctrine \ ORM \ PersistentCollection " – CriticalImpact

+0

@CriticalImpact He echado un vistazo a través del fuente para el componente de formulario 2.0, y no veo ninguna manera de realmente lograr el mismo efecto (las opciones no se almacenan a largo plazo en eso). Sin embargo, es posible que pueda hacerlo si puede vivir con siempre usando las opciones predeterminadas: para solucionar el error que está obteniendo arriba, debe simplemente establecer property_path de manera apropiada (desafortunadamente tendré que dejar que usted descubra qué formato 2.0 usa para las rutas de propiedad en las colecciones; sin embargo, pocos var_dump bien ubicados deberían hacer el truco) –

+2

De hecho, logré encontrar otra solución. Agregué un detector de eventos para FormEvents :: PRE_SET_DATA, obtuve el objeto de respaldo (en mi caso un objeto de pregunta), determiné el tipo de la pregunta (tengo algo configurado en mi que stion que indica si es una casilla de verificación, sí/no, campo de texto, etc.) y luego agregó el campo al formulario basado en el tipo que está establecido en el objeto de la pregunta. – CriticalImpact

0

En el ejemplo usted tiene facilite, se tendría que crear diferentes clases de formulario para aquellos ProductOrder y SubscriptionOrder

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

y

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

En su clase de forma OrderType se añaden estas dos formas, como este

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

Ahora esto agrega las dos formas SubsciptionOrderType, ProductOrder Escriba en la forma principal OrderType. Por lo tanto, más adelante, en el controlador, si inicializa este formulario obtendrá todos los campos de la suscripción y los formularios del producto con los del tipo de pedido.

Espero que responda a sus preguntas si aún no está claro, por favor revise la documentación para incrustar múltiples formularios aquí. http://symfony.com/doc/current/cookbook/form/form_collections.html

+2

De esta solución, ¿no podría solo tener un solo producto y/o una sola suscripción? Id prefiero tener una colección de objetos que podrían ser productos o suscripciones y dejar que Symfony decida qué tipo de formulario es apropiado para la entidad en la colección – vcetinick

Cuestiones relacionadas