I'm currently having trouble with shutting down some validation constraints if a certain option is selected in the form. This option is not related to the model and is not set using the data_class option.
My form contains two embedded address forms which are exactly the same as in one invoice address and one shipment address. If the alternative option is selected, I want to make the shipment address form required through validation. If no alternative option is selected the shipment address form requires no validation and needs to be left alone.
CustomerCheckoutForm
class CustomerCheckoutForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', 'hidden')
->add('firstName', 'text')
->add('nameAdditions', 'text')
->add('lastName', 'text')
->add('gender', 'choice', array('expanded' => true, 'choices' => array('m' => 'M', 'f' => 'F')))
->add('birthDate', 'date', array(
'required' => true,
'input' => 'string',
'widget' => 'choice',
'years' => range(DATE_CURRENT_YEAR - 80, DATE_CURRENT_YEAR - 18),
'empty_value' => array('year' => 'Year', 'month' => 'Month', 'day' => 'Day')
))
->add('invoiceAddress', new AddressForm($options['countryMapper']), array(
'label' => false,
'required' => true,
))
->add('alternative_shipment_address', 'choice', array(
'expanded' => true,
'choices' => array(0 => 'Delivery on the current address', 1 => 'Alternative shipping address'),
'mapped' => false,
'required' => true,
'label' => 'Delivery address',
'data' => 0,
))
->add('shipmentAddress', new AddressForm($options['countryMapper']), array(
'label' => false,
'required' => false,
))
->add('Continue', 'submit');
$this->registerListeners($builder);
}
private function registerListeners(FormBuilderInterface $builder)
{
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) {
$customer = $event->getData();
$form = $event->getForm();
if (!$customer || $customer->getId() === null) {
$form->add('password', 'password');
}
});
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'EHV\SharedBundle\Model\Customer',
'countryMapper' => null,
'validation_groups' => array(
'Default',
'checkout',
),
));
}
public function getName()
{
return 'customerCheckout';
}
}
AddressForm
class AddressForm extends AbstractType
{
private $countryMapper;
public function __construct(CountryMapper $countryMapper)
{
$this->countryMapper = $countryMapper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new CountryToIdTransformer($this->countryMapper);
$countries = $this->countryMapper->getAll();
$data[''] = 'Make a choice...';
/** #var Country $country */
foreach ($countries as $country) {
$data[$country->getId()] = $country->getName();
}
$builder
->add('id', 'hidden')
->add('streetName', 'text')
->add('streetNumber', 'text')
->add('streetNumberAddition', 'text')
->add('postalCode', 'text')
->add('city', 'text')
->add(
$builder->create('country', 'choice', array('choices' => $data))
->addModelTransformer($transformer)
);
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'EHV\SharedBundle\Model\Address',
));
}
public function getName()
{
return 'address';
}
}
Both models have their own constraints set through the function:
public static function loadValidatorMetadata(ClassMetaData $metadata) {}
Checkout Callback constraints http://symfony.com/doc/current/reference/constraints/Callback.html
You can read the value of your addresses and rise error if needed.
I managed to use a Form event listener with the Constraint Callback answered by Hpatoio, details below.
CustomerCheckoutForm
$builder->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) {
$form = $event->getForm();
$customer = $event->getData();
if ($form->get('alternative_shipment_address')->getData() === 0) {
$customer->setShipmentAddress(null);
}
});
CustomerConstraint
$callback = function($customer) use ($metadata) {
$metadata->addPropertyConstraint('invoiceAddress', new Valid());
/** #var Customer $customer */
if ($customer->getShipmentAddress() !== null) {
$metadata->addPropertyConstraint('shipmentAddress', new Valid());
}
};
$metadata->addConstraint(new Callback($callback));
Credits to Hpatoio for pointing out the right direction.
Related
I have two entities with a Many->Many relation , appli and service
I want to create a form that would allow me to edit, add, or remove relations .
Those entities are linked as follow ..
Appli.php :
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Service", mappedBy="applis")
*/
private $services;
public function getService() {
return $this->services;
}
Service.php :
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Appli", inversedBy="services")
* #ORM\JoinTable(name="service_to_app",
* joinColumns={#ORM\JoinColumn(name="services", referencedColumnName="id_service")},
* inverseJoinColumns={#ORM\JoinColumn(name="applis", referencedColumnName="id_apps")}
* )
*/
private $applis;
Notice that $services and $applis are arrayCollections .
Here is my formType :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$item = $builder->getData();
$builder /* ... */
->add('serviceCol', CollectionType::class, array(
'entry_type' => ServiceType::class,
'entry_options' => array('label' => false,),
'property_path' => 'service',
'allow_add' => true, 'allow_delete' => true,
));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => Appli::class,
));
}
and the serviceType :
class ServiceType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('service', EntityType::class, array(
'class' => Service::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('s')
->orderBy('s.id_service', 'ASC');},
'choice_label' => 'nom', 'property_path' => 'nom', 'label' => false));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => Service::class,
));
}
}
My issue so far is to preselect relations, I do have the correct amount of fields , but it's only default value and does not show the existing relations .
I could get to it without using the CollectionType field , but then , I would not be able to add / remove fields .
I cant fill the 'data' option since $item->getService() gives an arrayCollection , and I could not find a way to iterate through it during the Collection definition .
Is there any simple enough solution to this ?
So the fix was quite simple , the solution was on Symfony form - Access Entity inside child entry Type in a CollectionType
It also made me read How to Dynamically Modify Forms Using Form Events with a renewed attention !
here s is my ServiceType formBuilder :
$builder->addEventListener(FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($builder) {
$form = $event->getForm();
$service = $event->getData();
if($service instanceof Service) {
$form->add('service', EntityType::class, array(
'class' => Service::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('s')
->orderBy('s.id_service', 'ASC');},
'choice_label' => 'nom',
'property_path' => 'nom',
'label' => false,
'data' => $service ));
}
})
;
I have two pages and would like to use one uploadType. When I edit, show the name in the textfield. In NewsController I give the data.
$ EditForm ['upload'] ['filename'] -> setData ($ upload);
This is working. But when i will show the uploadPage and set the data in the uploadType.
'data' => $ build-> getData ()
so is the news textfield empty.
UploadType:
class UploadType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$data = $builder->getData();
if($this->isFieldEnabled('directory',$options)) {
$builder ->add('directory', null, array(
'label' => 'upload.directory',
'required' => true,
)
);
}
$builder->add('name', FileType::class,array(
'label' => 'upload.choice',
'data_class' => null,
'attr' => array(
'class' => 'hidden'
)
));
if($this->isFieldEnabled('filename',$options)) {
$builder->add('filename', 'text',array(
'label' => 'upload.upload',
'data' => $data, //If set, don't show in the news textfield
'mapped' => false,
));
}
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Maiskolben\UploadBundle\Entity\Upload',
'translation_domain' => 'Maiskolben',
'disableFields' => [],
)
);
$resolver->setOptional([
'disableFields',
]);
}
private function isFieldEnabled($fieldName, $options)
{
if (isset($options['disableFields']) && !empty($options['disableFields'])) {
if (in_array($fieldName, $options['disableFields'])) {
return false;
}
}
return true;
}
}
NewsType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class,array(
'label' => 'news.newsTitle',
))
->add('content','textarea',array(
'label' => 'news.content',
))
->add('upload', new UploadType(), array(
'label' => false,
'required' => false,
'disableFields' => array(
'directory',
)
));
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Maiskolben\NewsBundle\Entity\News',
'translation_domain' => 'Maiskolben',
));
}
}
NewsController:
public function editAction(Request $request, News $news)
{
$upload = $news->getUpload();
$editForm = $this->createForm(new NewsType, $news);
$editForm['upload']['filename']->setData($upload);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
$em = $this->getDoctrine()->getManager();
//....
I tryed with "addEventListener" in both site. All the same result.
I hope anyone can help me. :)
I am developing my first complex form with Symfony 2 and I have gotten stuck.
This image represents my situation
So, I have a very simple entity called TShirt with the following attributes:
name -> mapped as a string column in the database
tags_data -> mapped as a string column in the database (that will contain a JSON object)
Then I have a form with more fields than the entity has:
class TShirtType extends AbstractType {
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder
->add('name', 'text', array(
'required' => true,
))
->add( 'colours', 'choice', array(
'choices' => $availableColours,
'multiple' => true,
'expanded' => true,
'mapped' => false,
))
->add( 'sizes', 'choice', array(
'choices' => $availableSizes,
'multiple' => true,
'expanded' => true,
'mapped' => false,
))
->add( 'materials', 'choice', array(
'choices' => $availableMaterials,
'multiple' => true,
'expanded' => true,
'mapped' => false,
));
}
/**
* {#inheritdoc}
*/
public function setDefaultOptions( OptionsResolverInterface $resolver ) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\TShirt',
));
}
public function getName() {
return 'app_bundle_tshirt_form';
}
}
I have read about Data Transformers but as far as I know they cannot help me in this issue because they work with mapped fields.
Giving this scenario, I want to add something like the Data Transformers that takes the colours, sizes and materials form fields and builds the JSON object to store it in the Entity.
So my question is: Is there a way for custom handling the non-mapped fields in the form class?
Any help would be very appreciated! Thanks!!
Maybe you could use an EventListener in your FormType like below :
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$formFactory = $builder->getFormFactory();
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (\Symfony\Component\Form\FormEvent $event) use ($formFactory, $builder) {
$data = $event->getData();
$form->add(
'customFieldName',
'text',
array('some_options' => 'someOptionValue')
);
}
Be careful about FormEvents (here : PRE_SET_DATA).
Also, $data = $event->getData(); allows you to get the related object. You could loop on it and parse JSON (or create as much method as your JSON array contains and call them over and over) in order to display as much $form->add(.. as you have properties ion you JSON array.
it's not good solution to store it in tagsData !
solution for SYMFONY 2.7
Controller.php:
$colors = [
'white' => 'white',
'black' => 'black',
'green' => 'green'
];
$sizes = [
'M' => 'M',
'L' => 'L',
'XL' => 'XL'
];
$materials = [
'Cotton' => 'Cotton',
'Wool' => 'Wool',
'Silk' => 'Silk'
];
$object = new TShirt();
$form = $this->createForm(new TShirtType(), $object, ['colours' => $colors, 'sizes' => $sizes, 'materials' => $materials]);
$form->handleRequest($request);
TShirtType.php
class TShirtType extends AbstractResourceType
{
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder
->add('name', 'text', array(
'required' => true,
))
->add( 'colours', 'choice', array(
'choices' => $options['colours'],
'multiple' => true,
'expanded' => true,
'mapped' => false
))
->add( 'sizes', 'choice', array(
'choices' => $options['sizes'],
'multiple' => true,
'expanded' => true,
'mapped' => false
))
->add( 'materials', 'choice', array(
'choices' => $options['materials'],
'multiple' => true,
'expanded' => true,
'mapped' => false
));
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event)
{
$object = $event->getData();
$form = $event->getForm();
$tagsData = [
'colours' => $form->get('colours')->getData(),
'sizes' => $form->get('sizes')->getData(),
'materials' => $form->get('materials')->getData(),
];
$object->setTagsData($tagsData);
$event->setData($object);
});
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
$tagsData = $view->vars['data']->getTagsData();
if($tagsData) {
$types = ['colours', 'sizes', 'materials'];
foreach($types as $type) {
foreach($view->offsetGet($type) as $checkbox) {
$checkbox->vars['checked'] = isset($tagsData[$type]) && in_array($checkbox->vars['value'], $tagsData[$type]);
}
}
}
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver ) {
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\TShirShirt',
));
$resolver->setRequired(['colours', 'sizes', 'materials']);
}
public function getName() {
return 'app_bundle_tshirt_form';
}
}
I finally solved the problem by creating a custom DataMapper as this tutorial explains
https://webmozart.io/blog/2015/09/09/value-objects-in-symfony-forms/
I have 2 entities:
TransactionDefaultParametersEntity
Parameter: widgetType
Parameter: defaultParameters (Has one to one relationship with DefaultParametersEntity)
DefaultParametersEntity
Parameter: checkIn
Parameter: checkOut
Parameter: currency
Parameter: adults
The controller provides a collection of TransactionDefaultParametersEntities, and for each I want a form displayed with all DefaultParameterEntity parameters as well as the widgetType parameter provided by the TransactionDefaultParametersEntity.
Currently I have a form type for each entity:
TransactionDefaultParametersFormType
class DefaultSettingsFormType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('defaultParameters', 'collection', ['type' => default_parameters', 'allow_add' => true]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\TransactionDefaultParametersEntity',
'create' => false
));
}
public function getName()
{
return 'transaction_default_parameters';
}
}
DefaultParametersFormType
class DefaultParametersFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('widgetType', 'choice', [
'choices' => [
'Default',
'Booking Engine',
'Informative'
],
'label' => 'Widget Type'
])
->add('checkIn', 'text', ['label' => 'Check In'])
->add('checkOut', 'text', ['label' => 'Check Out'])
->add('currency', 'text', ['label' => 'Currency'])
->add('adults', 'text', ['label' => 'adults']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\DefaultParametersEntity',
));
}
public function getName()
{
return 'default_parameters';
}
}
And lastly in the controller:
// $transactionDefaultParametersCollection is a collection of TransactionDefaultParameterEntities
$form = $this->createFormBuilder($transactionDefaultParametersCollection)
->add('transactionDefaultParameters', 'transaction_default_settings')
->add('save', 'submit', array('label' => 'Save'))
->getForm();
$form->handleRequest($request);
if ($form->isValid()) {
// do something
}
return $this->render(
'settings/editDefaults.html.twig',
[
'form' => $form->createView()
]
);
If this wasn't clear please let me know :). Also if I could be pointed to an article that would be very helpful. I looked at how to embed a collection of forms but this was a different case than the example provided.
I've this Form:
OrdersType
class OrdersType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Others $builder fields goes here
if ($this->register_type[0] == "natural")
{
$builder->add('person', new NaturalPersonType(), array('label' => FALSE));
}
elseif ($this->register_type[0] == "legal")
{
$builder->add('person', new LegalPersonType(), array('label' => FALSE));
}
}
}
PersonType
class PersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', 'text', array(
'required' => TRUE,
'label' => FALSE
))
->add('contact_person', 'text', array(
'required' => FALSE,
'label' => 'Persona de Contacto'
));
}
}
This is what I'm doing in the controller:
public function editAction($id = null)
{
$em = $this->getDoctrine()->getManager();
$order = $em->getRepository('FrontendBundle:Orders')->find($id);
$type = $order->getPerson()->getPersonType() === 1 ? "natural" : "legal";
$orderForm = $this->createForm(new OrdersType(array($type)), $order, array(
'action' => $this->generateUrl('update-order', array('id' => $id)),
'method' => 'POST',
));
return array(
'entity' => $order,
"form" => $orderForm->createView(),
'id' => $id
);
}
Then in my view I'm rendering fields as follow:
{{ form_widget(form.person.person.description) }}
The code renders the field right but without values and yes, it has values, where is the error? This are the others Forms I'm using:
LegalPersonType
class LegalPersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rif', 'text', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 10,
))
)
->add('identification_type', 'choice', array(
'label' => FALSE,
'choices' => RifType::getChoices(),
'attr' => array(
'class' => 'toSelect2'
)
))
->add('person', new PersonType(), array('label' => FALSE));
}
}
Working on mapping
PersonType
class PersonType extends AbstractType {
....
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Person',
'inherit_data' => true
));
}
public function getName()
{
return 'person';
}
}
NaturalPersonType
class NaturalPersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('identification_type', 'choice', array(
'label' => 'Número de Cédula',
'choices' => CIType::getChoices()
))
->add('ci', 'number', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 8,
))
)
->add('person', new PersonType(), array(
'label' => FALSE,
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
public function getName()
{
return 'natural_person';
}
}
If I remove the setDefaultOptions method from NaturalPersonType I get this error:
The form's view data is expected to be of type scalar, array or an
instance of \ArrayAccess, but is an instance of class
Tanane\FrontendBundle\Entity\NaturalPerson. You can avoid this error
by setting the "data_class" option to
"Tanane\FrontendBundle\Entity\NaturalPerson" or by adding a view
transformer that transforms an instance of class
Tanane\FrontendBundle\Entity\NaturalPerson to scalar, array or an
instance of \ArrayAccess.
If I leave as it's I get this other:
Method "description" for object "Symfony\Component\Form\FormView" does
not exist in
/var/www/html/tanane/src/Tanane/BackendBundle/Resources/views/Order/edit.html.twig
at line 134
It looks like PersonType is not being mapped correctly to your entity.
Since it's being used both by LegalPersonType and NaturalPersonType to handle some of their properties, I would define it as parent of those two. This can be achieved by using the inherit_data option (documentation).
Your code would look something like this:
PersonType
class PersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('description', 'text', array(
'required' => TRUE,
'label' => FALSE
))
->add('contact_person', 'text', array(
'required' => FALSE,
'label' => 'Persona de Contacto'
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'inherit_data' => true,
));
}
}
Note that you should remove the data_class option from setDefaultOptions().
LegalPersonType
class LegalPersonType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('rif', 'text', array(
'required' => true,
'label' => false,
'attr' => array(
'maxlength' => 10,
))
)
->add('identification_type', 'choice', array(
'label' => FALSE,
'choices' => RifType::getChoices(),
'attr' => array(
'class' => 'toSelect2'
)
))
->add('data', new PersonType(), array(
'label' => FALSE,
'data_class' => 'YourBundle\Entity\LegalPerson',
));
}
}
Your NaturalPersonType would look similar to LegalPersonType.
Now, you can do this in your view:
{{ form_widget(form.person.data.description) }}