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";
}
}
¿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
@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) –
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