Where to normalize submitted form values in Symfony forms? - symfony

I have a postal code field in my form, which' value should match this regex: /^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/
In my entity I would like all postal codes to have the same format: 4 digits, a space and 2 uppercase letters, so the incoming value needs to be normalized somewhere.
Question: where do I do this conversion? I'm using Symfony's form system and Symfony version 5.4.9.
Entity:
class Address
{
/**
* #ORM\Column(type="string", length=7)
* #Assert\NotBlank
* #Assert\Regex(
* pattern="/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/",
* message="Deze waarde is geen geldige postcode."
* )
*/
private $postcode;
public function setPostcode(string $postcode): self
{
$this->postcode = $postcode;
return $this;
}
// other fields
}
FormType:
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('postcode', null, [
'label' => 'Postcode'
])
// other fields
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Address::class,
]);
}
}
If I understand correctly, the form system sets incoming valid and invalid values from the form directly onto my entity (in the isSubmitted method in the controller), and after that the entity is validated (in the isValid method).
So if I add the normalization in my entity's setter (which is called in isSubmitted), I would have to manually validate the incoming value before I can normalize it, but this duplicates the regex that's later executed by isValid. Same goes if I were to use an event listener or a transformer on the form, so neither of these seem to be a great solution.
How is this usually done?

I ended up using an event listener as that required the least amount of extra code.
Restricted the regex on the entity like this:
class Address
{
/**
* #ORM\Column(type="string", length=7)
* #Assert\NotBlank
* #Assert\Regex(
* pattern="/^[1-9][0-9]{3} [A-Z]{2}$/",
* message="Deze waarde is geen geldige postcode."
* )
*/
private $postcode;
public function setPostcode(string $postcode): self
{
$this->postcode = $postcode;
return $this;
}
// other fields
}
And added an event listener for the PRE_SUBMIT event on the FormType:
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('postcode', null, [
'label' => 'Postcode'
])
// other fields
;
$builder->get('postcode')->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!\is_string($data)) {
return;
}
if (preg_match('/^([1-9][0-9]{3})\s?([a-zA-Z]{2})$/', $data, $matches)) {
$event->setData($matches[1] . ' ' . strtoupper($matches[2]));
}
});
}
}
This does change the formatting of the value that is displayed in the form in case of a submit + invalid, but I did not mind that.
If you want to keep the displayed value exactly as the user inputted it, but transform the value in the background before it is set onto the entity, you can use a ModelTransformer instead:
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('postcode', null, [
'label' => 'Postcode'
])
// other fields
;
$builder->get('postcode')
->addModelTransformer(new CallbackTransformer(
function ($modelData) {
return $modelData;
},
function ($normData) {
if (!\is_string($normData)) {
return;
}
if (preg_match('/^([1-9][0-9]{3})\s?([a-zA-Z]{2})$/', $normData, $matches)) {
return $matches[1] . ' ' . strtoupper($matches[2]);
}
return $normData;
}
))
;
}
}
Using a ViewTransformer (->addViewTransformer) with the same code as the ModelTransformer above also changes the formatting of the value that is displayed in the form, but with more code than the event listener.

Related

Symfony 2.7: Automatically Populate Form Field from Within Form Builder

I have an entity named HoursSpecial with a foreign key relationship to an entity called HoursArea. Each HoursSpecial belongs to an HoursArea. When I create a new HoursSpecial via my HoursSpecialType, I want the form field to automatically populate the HoursArea field.
I know what you're thinking, just do something like this in my controller's method:
$form->add('area', 'hidden', array('data'=>$area));
That would be fine except I need to make a DataTransformer to switch between the area's ID and the actual area entity. So I have to declare my HoursArea field within my HoursSpecialType with the transformer:
$builder
...
->add('area', 'hidden')
;
$builder->get('area')->addModelTransformer(new HoursAreaToIntTransformer($this->manager));
Now, I can't simply feed my HoursArea entity into the form. Is there an effective way to make this happen?
I've thumbed through Symfony's documentation on How to Dynamically Modify Forms Using Form Events, but I can't make heads or tails of how I would pass in that HoursArea entity dynamically from outside of the form builder. Maybe I'm just missing something?
UPDATE
Following the recommendation of the answer (Recommendation #1) below from #Ryan, I have created the custom type HiddenHoursAreaType:
// AppBundle\Form\Type\HideenHoursAreaType.php
class HiddenHoursAreaType extends AbstractType
{
//need to instantiate HoursAreaToIntTransformer
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new HoursAreaToIntTransformer($this->manager);
$builder->addModelTransformer($transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => null,
'compound' => true //this should be FALSE as there are no children
));
}
/**
*
* the return value of the getParent function indicates that you're extending the choice field type.
* This means that, by default, you inherit all of the logic and rendering of that field type.
*/
public function getParent()
{
return 'hidden';
}
public function getName()
{
return 'app_hoursArea';
}
I have added my transformer into the custom type class. Here is the transformer class:
// AppBundle\Form\DataTransformer;
class HoursAreaToIntTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $manager)
{
$this->manager = $manager;
}
/**
* Transforms an object (HoursArea) to a string (number).
*
* #param Issue|null $issue
* #return string
*/
public function transform($area)
{
if (null === $area) {
return '';
}
return $area->getId();
}
/**
* Transforms a string (number) to an object (HoursArea).
*
* #param string $areaId
* #return HoursArea|null
* #throws TransformationFailedException if object (HoursArea) is not found.
*/
public function reverseTransform($areaId)
{
// no area number? It's optional, so that's ok
if (!$areaId) {
return;
}
$area = $this->manager
->getRepository('AppBundle:HoursArea')
// query for the issue with this id
->find($areaId)
;
if (null === $area) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'An area with number "%s" does not exist!',
$areaId
));
}
return $area;
}
}
Within my controller I create the form with the custom type field:
$form = $this->createForm(new HoursSpecialType($this->getDoctrine()->getManager()), $entity, array(
'action' => $this->generateUrl('hoursspecial_postcreate'),
'method' => 'POST',
));
$form->add('eventDate', 'hidden', array('data'=>$dateString));
$form->add('area', new \AppBundle\Form\Type\HiddenHoursAreaType($this->getDoctrine()->getManager()), array(
'data'=>$area,
'invalid_message'=>'Area field not converted proerly'
));
$form->add('submit', 'submit', array('label' => 'Create'));
Thanks to the transformer and the custom type, the form now correctly converts the HoursArea entity to an integer for population in the hidden field.
The problem now is that upon form submission, the integer is not converted back into an HoursArea object. I know this because I get the 'invalid_message' upon submission.
Final Update
The reason the HoursArea id wasn't being inserted properly had something to do with the
'compound' => true
setting I had in my custom type. I assume it was looking for child fields and wasn't finding any...which it shouldn't have because there were none!
You could create a custom type for it and add the addModelTransformer() call in the buildForm() of your custom type, but still pass the data in explicitly. So your $form->add('area', 'hidden', array('data'=>$area)) would become $form->add('area', new HiddenHoursAreaType(), array('data'=>$area)) where HiddenHoursAreaType::getParent() would be the hidden type.
You could set the data in a POST_SET_DATA listener.
You could get the $options['data'] value in buildForm() and explicitly pass in the HoursArea ID.
/** #var HoursSpecial $hoursSpecial Prepopulated in controller */
$hoursSpecial = $options['data']
$builder->add('area', 'hidden', ['data' => $hoursSpecial->getHoursArea()->getId()])

Model transformer and expected form view data mismatch

I have a Symfony 2 application, with a form that needs to store a reference to another entity (project) in a hidden field. The project entity is passed in via the form options, my plan was to have a field of the type 'hidden' that simply contains the entity id, this should then be transformed into a project entity on when the form is submitted.
I'm going about this by using a model transformer to transform between the entity to a string (it's ID). However when I try to view the form, I get the following error:
The form's view data is expected to be an instance of class Foo\BarBundle\Entity\Project, but is a(n) string. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms a(n) string to an instance of Foo\BarBundle\Entity\Project.
Here is my form class:
<?php
namespace Foo\BarBundle\Form\SED\Waste;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Foo\BarBundle\Form\DataTransformer\EntityToEntityIdTransformer;
/**
* Class WasteContractorEntryType
* #package Foo\BarBundle\Form\CommunityInvestment\Base
*/
class WasteContractorEntryType extends AbstractType
{
protected $name;
protected $type;
protected $phase;
protected $wasteComponent;
public function __construct($formName, $type, $phase, $wasteComponent)
{
$this->name = $formName;
$this->type = $type;
$this->phase = $phase;
$this->wasteComponent = $wasteComponent;
}
/**
* #return mixed
*/
public function getType()
{
return $this->type;
}
/**
* #return mixed
*/
public function getPhase()
{
return $this->phase;
}
/**
* #return mixed
*/
public function getProject()
{
return $this->project;
}
/**
* #return mixed
*/
public function getWasteComponent()
{
return $this->wasteComponent;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$em = $options['em'];
$wasteComponentTransformer = new EntityToEntityIdTransformer($em,
'Foo\BarBundle\Entity\SED\Waste\WasteComponent');
$projectTransformer = new EntityToEntityIdTransformer($em, 'Foo\BarBundle\Entity\Project');
$builder->add('id', 'hidden');
$builder->add(
$builder->create('project', 'hidden', array(
'data' => $options['project'],
'by_reference' => false
))
->addModelTransformer($projectTransformer)
);
$builder->add(
$builder->create('wasteComponent', 'hidden', array(
'data' => $this->getWasteComponent()
))
->addModelTransformer($wasteComponentTransformer)
);
$builder->add('phase', 'hidden', array(
'data' => $this->getPhase()
));
$builder->add('type', 'hidden', array(
'data' => $this->getType()
));
$builder->add('percentDivertedFromLandfill', 'text', array());
$builder->add('wasteContractor', 'entity', array(
'class' => 'Foo\BazBundle\Entity\Contractor',
'property' => 'name',
'attr' => array(
'class' => 'js-select2'
)
));
}
public function getName()
{
return $this->name;
}
/**
* {#inheritDoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => true,
'data_class' => 'Foo\BarBundle\Entity\SED\Waste\WasteContractorEntry'
))
->setRequired(array(
'em',
'project'
))
->setAllowedTypes(array(
'em' => 'Doctrine\Common\Persistence\ObjectManager',
'project' => 'Foo\BarBundle\Entity\Project'
));
}
}
And my model transformer class:
<?php
namespace Foo\BarBundle\Form\DataTransformer;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
class EntityToEntityIdTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $om;
/**
* #var Entity class
*/
protected $entityClass;
/**
* #param ObjectManager $om
*/
public function __construct(ObjectManager $om, $className)
{
$this->om = $om;
$this->entityClass = $className;
}
protected function getEntityClass()
{
return $this->entityClass;
}
/**
* Transforms an object (project) to a string (id).
*
* #param Project|null $issue
* #return string
*/
public function transform($entity)
{
if (null === $entity) {
return "";
}
return $entity->getId();
}
/**
* Transforms a string (id) to an object (project).
*
* #param string $id
*
* #return Issue|null
*
* #throws TransformationFailedException if object (project) is not found.
*/
public function reverseTransform($id)
{
if (!$id) {
return null;
}
$entity = $this->om
->getRepository($this->getEntityClass())
->find($id);
if (null === $entity) {
throw new TransformationFailedException(sprintf(
'An entity of class %s with id "%s" does not exist!',
$this->getEntityClass(),
$id
));
}
return $entity;
}
}
I've tried using a adding the transformer as a view transformer instead of a model transformer, however then I just get a slightly different error:
The form's view data is expected to be an instance of class
Foo\BarBundle\Entity\Project, but is a(n) integer. You can avoid this
error by setting the "data_class" option to null or by adding a view
transformer that transforms a(n) integer to an instance of
Foo\BarBundle\Entity\Project.
It seems that setting 'data_class' to null as suggested by the exception message above is the solution. I had previously rejected this as it seems counter-intuitive when we know that the purpose of the field is to reference a project entity.
With the 'data_class' option set to null, the hidden project field contains the project id, and upon submission, calling getProject() on the created entity returns the correct project object.

Symfony 2 forms with validation groups, errors mapped to the wrong property?

Never had this problem before.
Fill the form with a phone, leaving lastname blank
Submit the form (and the validation groups become Default and Create)
The error "Last name is required." is mapped on the wrong $phone field, while should be mappend to $lastName itself property
Can you reproduce the same issue?
$phone property is in the Create validation group, while $phone in Default implicit group:
class User
{
/**
* #Assert\NotBlank(groups={"Create"}, message="Last name is required.")
*
* #var string
*/
protected $lastName;
/**
* #Assert\NotBlank(message="Phone is required.")
*
* #var string
*/
protected $phone;
}
I determine the validation groups based on submitted data:
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('lastName', 'text');
$builder->add('phone', 'text');
$builder->add('submit', 'submit');
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'required' => false,
'data_class' => 'Acme\HelloBundle\Entity\User',
'validation_groups' => function (FormInterface $form) {
return null === $form->getData()->getId()
? ['Default', 'Create']
: ['Default', 'Edit'];
}
]);
}
}
Instead of using a compiler pass, you can edit config.yml to set the API to 2.4 :
validation:
enable_annotations: true
api: 2.4 # default is auto which sets API 2.5 BC
When the bug is resolved in 2.5, just remove the api setting and you will get back to 2.5 backward compatible.
Warning there is a bug with validation API 2.5
Took a couple of hours but I found it! Actually is an issue (https://github.com/symfony/symfony/issues/11003) for the new validator API 2.5.
Temporary solution (compiler pass):
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\Validation;
class SetValidatorBuilderApiVersionWorkaround implements CompilerPassInterface
{
/**
* {#inheritDoc}
*/
public function process(ContainerBuilder $container)
{
// TODO remove when https://github.com/symfony/symfony/issues/11003
// is fixed (validation errors added to the wrong field)
$container->getDefinition('validator.builder')
->addMethodCall('setApiVersion', [Validation::API_VERSION_2_4]);
}
}

Two form fields into one entity value

How its possible to join two separated fields (must be separated) in one form (date and time for example) to one entity propery datetime for persisting after form post ?
What is better way ? Data Transofmers ? Form events ? Form Model ? Manual setting all entity properties before persist ?
Entity:
<?php namespace Acme\DemoBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="event")
*/
class EventEntity
{
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
// ...
/**
* #ORM\Column(name="date_time", type="datetime", nullable=false)
*/
protected $datetime;
public function getId()
{
return $this->id;
}
// ...
public function getDateTime()
{
return $this->datetime;
}
public function setDateTime(\DateTime $datetime)
{
$this->datetime = $datetime;
}
}
FormType:
<?php namespace Acme\DemoBundle\Form\Type;
use JMS\DiExtraBundle\Annotation as DI;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class EventType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('date', 'date', [
'required' => true,
'widget' => 'single_text',
'format' => 'dd.MM.yyyy'
]
)
->add('time', 'time', [
'required' => false,
'widget' => 'single_text'
]
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\EventEntity' //Acme\DemoBundle\Form\Model\EventModel ?
));
}
public function getName()
{
return 'event';
}
}
If you set the date and time widget seperately in the datetime type, then they get seperately rendered, but validated as one field.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('datetime', 'datetime', array(
'date_widget' => 'single_text',
'time_widget' => 'single_text',
'date_format' => 'dd.MM.yyyy',
));
}
I suggest using Pazis solution, since this is the most simple one. But that would also be a perfect job for a DataTransformer:
class MyDataTransformer implements DataTransformerInterface
{
public function transform($value)
{
if (null === $value)
return;
if ($value instanceof \DateTime)
return array(
'date' => $value->format('d.m.Y'),
'time' => $value->format('H:i:s')
);
return null;
}
public function reverseTransform($value)
{
if (null === $value)
return null;
if (is_array($value) && array_key_exists('date', $value) && array_key_exists('time', $value))
return new \DateTime($value['date'] . ' ' . $value['time']);
return null;
}
}
This has the drawback, that you'd need to map every single value in your entity with this transformer, what - for sure - you don't want to. But with small form-tricks, this can be avoided. Therefore you add a subform to your form, which includes a date and a time field and the added Transformer. You'll need to map ("property_path"-option) your DateTime object to this subform or just name it "correctly", so the form framework can map it by name.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add(
$builder->create('datetime', 'form')
->add('date', 'date', $optionsForDate)
->add('time', 'time', $optionsForTime)
->addViewTransformer(new MyDataTransformer())
);
}
The code may not be perfectly running, but i hope the idea behind splitting one entity property into two (or more) form fields is clear.
héhé, that's a good question.
I would choose the easiest, most generic, reusable solution.
I wouldn't implement methods on my model just for sake of form mapping, but if it makes sense, why not simply using the model api ?
<?php
class EventEntity
{
// assume $this->datetime is initialized and instance of DateTime
public function setDate(\DateTime $date)
{
// i don't know if this works!
$this->datetime->add($this->datetime->diff($date));
}
public function setTime(\DateTime $date)
{
$this->datetime->add($this->datetime->diff($date));
}
}

Symfony2 Forms and Polymorphic collections

Im playing around with Symfony2 and Im abit unsure how Symfony2 handles Polymorphic collections in the View component. It seems that i can create an entity with a collection of AbstractChildren, but not sure how to what to do with it inside a Form Type class.
For example, I have the following entity relationship.
/**
* #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;
...
}
Simple enough, but when im create a form for my order class
class OrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder->add('items', 'collection', array('type' => AbstractOrderItemType()));
}
}
I am unsure how to handle this situation where you effectively need a different Form Type for each class of item in the collection?
I recently tackled a similar problem - Symfony itself makes no concessions for polymorphic collections, but it's easy to provide support for them using an EventListener to extend the form.
Below is the content of my EventListener, which uses a similar approach to Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener, the event listener which provides the collection form type's normal functionality:
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));
}
}
}
The downside to using this approach is that for it to recognize the types of your polymorphic entities on submit, you must set the data on your form with the relevant entities before binding it, otherwise the listener has no way of ascertaining what type the data really is. You could potentially work around this working with the FormTypeGuesser system, but that was beyond the scope of my solution.
Similarly, while a collection using this system still supports adding/removing rows, it will assume that all new rows are of the base type - if you try to set them up as extended entities, it'll give you an error about the form containing extra fields.
For simplicity's sake, I use a convenience type to encapsulate this functionality - see below for that and an example:
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";
}
}
Example:
namespace 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";
}
}
In the example you have give, you would have to create different form class for those ProductOrder and SubscriptionOrder
class ProductOrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
//Form elements related to Product Order here
}
}
and
class SubsciptionOrderType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
//Form elements related SubscriptionOrder here
}
}
In your OrderType form class you add both these forms, like this
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
}
}
Now this adds the two forms SubsciptionOrderType,ProductOrderType to the main form OrderType . So later in the controller if you initialize this form you will get all the fields of the subscription and product forms with that of the OrderType.
I hope this answers your questions if still not clear please go through the documentation for embedding multiple forms here. http://symfony.com/doc/current/cookbook/form/form_collections.html

Resources