2012-07-16 1 views
11

Ich spiele mit Symfony2 und ich bin nicht sicher, wie Symfony2 polymorphe Sammlungen in der View-Komponente behandelt. Es scheint, dass ich eine Entity mit einer Sammlung von AbstractChildren erstellen kann, aber nicht sicher bin, was ich damit in einer Form Type Klasse machen soll.Symfony2 Formen und polymorphe Sammlungen

Zum Beispiel habe ich die folgende Entity-Beziehung.

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

Einfach genug, aber wenn im Erstellen Sie ein Formular für meine Bestellung Klasse

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

Ich bin nicht sicher, wie mit dieser Situation umgehen, wo man effektiv einen anderen Formulartyp für jede Klasse von Artikel müssen in der Sammlung?

Antwort

9

Ich habe vor kurzem ein ähnliches Problem angegangen - Symfony selbst macht keine Zugeständnisse für polymorphe Sammlungen, aber es ist einfach, sie mit einem EventListener zu unterstützen, um das Formular zu erweitern.

Unten ist der Inhalt meiner Eventlistener, die Symfony \ Component \ Formular \ Extension \ Core \ Eventlistener \ ResizeFormListener, den Ereignis-Listener, der die Form Typ der normalen Funktionalität Sammlung bietet einen ähnlichen Ansatz verwendet:

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

Der Nachteil dieses Ansatzes besteht darin, dass für die Erkennung der Typen Ihrer polymorphen Entitäten beim Senden die die Daten in Ihrem Formular mit den relevanten Entitäten vor dem Binden festlegen müssen, andernfalls hat der Listener keine Möglichkeit, festzustellen, um welchen Typ es sich handelt Die Daten sind wirklich. Sie könnten diese Arbeit mit dem FormTypeGuesser-System möglicherweise umgehen, aber das war nicht der Rahmen meiner Lösung.

Während eine Sammlung, die dieses System verwendet, weiterhin das Hinzufügen/Entfernen von Zeilen unterstützt, wird davon ausgegangen, dass alle neuen Zeilen vom Basistyp sind. Wenn Sie versuchen, sie als erweiterte Entitäten einzurichten, erhalten Sie einen Fehler über das Formular mit zusätzlichen Feldern.

Aus Gründen der Einfachheit, verwende ich einen Typ der Zweckmäßigkeit diese Funktionalität zu kapseln - siehe unten für das und ein Beispiel:

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

Beispiel: Namespace Acme \ VariedCollectionBundle \ Formular;

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

Haben Sie eine Idee, wie Sie Symfony2 dazu bringen können? Insbesondere "$ child-> getConfig() -> getOptions();" ist in 2.0 nicht verfügbar und daher kann ich die ursprünglichen Optionen für das Formular nicht abrufen. Wenn ich die Optionen auslasse, bekomme ich schließlich "Weder Eigenschaft 0" noch Methode "get0()" oder Methode "is0()" existiert in der Klasse "Doctrine \ ORM \ PersistentCollection" – CriticalImpact

+0

@CriticalImpact Ich habe einen Blick durch die Quelle für die 2.0 Form-Komponente, und ich kann keine Möglichkeit sehen, den gleichen Effekt wirklich zu erreichen (die Optionen sind nicht auf lange Sicht in diesem gespeichert.) Sie können vielleicht tun, obwohl wenn Sie damit leben können Immer die Standardoptionen verwenden - um den Fehler, den Sie oben bekommen, zu lösen, sollten Sie nur property_path entsprechend setzen (leider muss ich Ihnen überlassen, um herauszufinden, welches Format 2.0 für Eigenschaftspfade in Sammlungen verwendet - a ein paar gut platzierte var_dumps sollten den Trick aber tuen) –

+2

Ich schaffte es tatsächlich, eine andere Lösung zu finden: Ich habe einen Event-Listener für FormEvents :: PRE_SET_DATA hinzugefügt, das Backing-Objekt (in meinem Fall ein Frageobjekt) bekommen, den Typ bestimmt die Frage (Ich habe etwas in meinem que gesetzt Das hängt davon ab, ob es ein Kontrollkästchen ist (ja/nein, Textfeld usw.), und fügt dann das Feld dem Formular hinzu, das auf dem Typ basiert, der im Frageobjekt festgelegt ist. – CriticalImpact

0

Im Beispiel Sie geben haben, würden Sie unterschiedliche Formularklasse für diejenigen ProductOrder und SubscriptionOrder

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

und

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

In Ihrer Auftragsart Form-Klasse erstellen müssen Sie diese Formen fügen Sie beide, wie dieser

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

Jetzt fügt das die beiden Formen SubscriptionOrderType, ProductOrder hinzu Geben Sie das Hauptformular OrderType ein. Wenn Sie dieses Formular später im Controller initialisieren, erhalten Sie alle Felder der Abonnement- und Produktformulare mit denen des OrderType.

Ich hoffe, dies beantwortet Ihre Fragen, wenn noch nicht klar, bitte gehen Sie durch die Dokumentation zum Einbetten mehrerer Formulare hier. http://symfony.com/doc/current/cookbook/form/form_collections.html

+2

Von dieser Lösung obwohl würde ich nicht nur in der Lage sein, ein einzelnes Produkt und/oder einzelnes Abonnement zu haben? Ich bevorzuge eine Sammlung von Objekten, bei denen es sich um Produkte oder Abonnements handeln kann, und Symfony entscheidet, welcher Formulartyp für die Entität in der Sammlung geeignet ist. – vcetinick