Symfony Embedded Form Conditional Validation - php

I have a form which contains three objects:
$builder
->add('customer', new CustomerType())
->add('shippingAddress', new AddressType())
->add('billingAddress', new AddressType())
->add('sameAsShipping', 'checkbox', ['mapped' => false])
;
Each of the embedded forms has their own validation constraints and they work. In my main form, I have cascade_validation => true so that all of the embedded form validation constraints are applied. This also works.
I am having trouble 'disabling' the validation on the billingAddress form if the sameAsShipping checkbox is enabled. I can't make the validation in the AddressType form conditional because it always needs to be enforced for the shippingAddress form.

I've solved this same problem by using validation groups.
First, this is important: use the validation_groups option in your AddressType to set the validation groups of every constraint of each field in the type:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\FormBuilderInterface;
class AddressType extends \Symfony\Component\Form\AbstractType
{
function buildForm(FormBuilderInterface $builder, array $options)
{
$groups = $options['validation_groups'];
$builder->add('firstName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
$builder->add('lastName', 'text', ['constraints' => new Assert\NotBlank(['groups' => $groups])]);
}
}
Then, in the parent form pass different validation groups to the two fields:
<?php
$formBuilder = $this->get('form.factory')
->createNamedBuilder('checkout', 'form', null, [
'cascade_validation' => true,
])
->add('billingAddress', 'address', [
'validation_groups' => 'billingAddress'
])
->add('shippingAddress', 'address', [
'validation_groups' => 'shippingAddress'
]);
Then, determine determine your validation groups by looking at the value of the checkbox.
if ($request->request->get('sameAsShipping')) {
$checkoutValidationGroups = ['Default', 'billingAddress'];
} else {
$checkoutValidationGroups = ['Default', 'billingAddress', 'shippingAddress'];
}
You can then validate only either the billingAddress or the shippingAddress, or both using the validation group mechanism.
I chose to use a button:
$formBuilder->add('submitButton', 'submit', ['validation_groups' => $checkoutValidationGroups]);

Create a form model (I use it in nearly every form, but this code here is not tested):
/**
* #Assert\GroupSequenceProvider()
*/
class YourForm implements GroupSequenceProviderInterface {
/**
* #Assert\Valid()
*/
private $customer;
/**
* #Assert\Valid()
*/
private $shippingAddress;
/**
* #Assert\Valid(groups={'BillingAddressRequired'})
*/
private $billingAddress;
private $billingSameAsShipping;
public function getGroupSequence() {
$groups = ['YourForm'];
if(!$this->billingSameAsShipping) {
$groups[] = 'BillingAddressRequired';
}
return $groups;
}
}
Try to use meaningful names. sameAsShipping is hard to understand. Read the if-condition in getGroupSequence: if not billing (address) same as shipping (address) then billing address required.
That's all, clear code in my opinion.

Related

How to submit array to an unmapped form field in symfony 4

I'm trying to submit array with values to a symfony 4 form field but the validation keeps failing.
I'm in the process of updating my application from symfony 2.7 to symfony 4. The problem is that a form that I used to use now always fails validation due to changes in symfony forms.
The symfony form has the following field
$builder->add('contactData', null, ['mapped' => false])
In symfony 2.7 I would always submit a POST request with array values in the contactData field and since it's not mapped it would just set the data to the field object in the submit process and the values were accessed in the Handler. Example request:
{
"name": {
"aField": "aValue",
"contactData": {
"something": "value"
}
}
}
However in symfony 4 there is now an added validation check in the \Symfony\Component\Form\Form class
} elseif (\is_array($submittedData) && !$this->config->getCompound() && !$this->config->hasOption('multiple')) {
that causes the validation to fail when submiting data to the contactData field, since the submittedData is indeed an array. I've been looking all over the internet and reading through the documentation of symfony but I can't seem to find a way to induce the same behavior as in symfony 2.7.
I would much appreciate any advice, I've been stuck on this for a while
Symfony has changed from v2.7 to 4.0, there is a lot of default values changed;
I faced the same problem and after 2 hours of investigations,
I ended up adding the attributes compound and allow_extra_field.
So, this should solve your problem:
$builder->add('contactData', null, [
'mapped' => false,
'compound' => true,
'allow_extra_fields' => true,
])
EDIT:
This didn't work as expected, I ended up with no error and no content as a submitted data, so I created a new type to add fields dynamically on a pre-submit event as following:
UnstructuredType.php
<?php
namespace ASTechSolutions\Bundle\DynamicFormBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
/**
* Class UnstructuredType.
*
* This class is created to resolve the change of form's behaviour introduced in https://github.com/symfony/symfony/pull/29307
* From v3.4.21, v4.1.10 and v 4.2.2, Symfony requires defining fields and don't accept arrays on a TextType for ex.
* TODO: this is a temporary solution and needs refactoring by declaring explicitly what fields we define, and then accept on requests
*
*/
class UnstructuredType extends AbstractType
{
/**
* {#inheritDoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$this->addChildren($event->getForm(), $event->getData());
});
}
/**
* #param FormInterface $form
* #param $data
*/
public function addChildren(FormInterface $form, $data)
{
if (is_array($data)) {
foreach ($data as $name => $value) {
if (!is_array($value)) {
$form->add($name);
} else {
$form->add($name, null, [
'compound' => true
]);
$this->addChildren($form->get($name), $value);
}
}
} else {
$form->add($data, null, [
'compound' => false,
]);
}
}
}
No need #sym183461's UnstructuredType in the other answer.
The information is in the extra fields.
You define the field like #sym183461 said:
$builder->add('contactData', null, [
'mapped' => false,
'compound' => true,
'allow_extra_fields' => true,
])
And then you can do this:
$contactData = $form->get('contactData')->getExtraFields()
All your data is in there, and it works with deep structures just fine.

TransformationFailedException when entity is passed to Form

I'm using Symfony 3.3 and im getting this TransformationFailedException Error, when i load my profile page:
Unable to transform value for property path "postalcode": Expected a numeric.
The postalcode value for this user in the database is:
'34125abc'
The postalcode attribute defined in UserProfile Entity:
/**
* #ORM\Column(type="string")
*/
private $postalcode;
My ProfileController:
class ProfileController extends Controller{
/**
* #Route("/edit_profile", name="edit_profile")
*/
public function profileAction(Request $request){
$profile = $this->getDoctrine()->getManager()->getRepository('AppBundle:UserProfile')->findOneBy(['user_id' => $this->getUser()->getUserId()]);
// If no UserProfile exists, create a UserProfile Object to insert it into database after POST
if(null === $profile){
$profile = new UserProfile();
$profile->setUserId($this->getUser()->getUserId());
}
$form = $this->createForm(EditProfileFormType::class);
$form->setData($profile);
// only handles data on POST
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()) {
$result = $this->forward('AppBundle:API\User\Profile:update_profile', array(
'profile' => $profile
));
if(200 === $result->getStatusCode()){
$this->addFlash('success', "Profile successfully created!");
}
}
return $this->render(':User/Profile:edit_profile.html.twig', [
'EditProfileForm' => $form->createView(),
]);
}
}
My EditProfileFormType:
class EditProfileFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', ChoiceType::class, array(
'choices' => array(
'Mr' => 'Mr',
'Mrs' => 'Mrs'
)
))
->add('firstName')
->add('lastName')
->add('street')
->add('postalcode', NumberType::class)
->add('city')
->add('telephone')
->add('mobile')
->add('company')
->add('birthday' , BirthdayType::class)
->add('callback', CheckboxType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\UserProfile',
'validation_groups' => array('edit_profile')
]);
}
public function getBlockPrefix()
{
return 'app_bundle_edit_profile_form_type';
}
}
So the problem here seems to be the not numeric string value in the databse
'34125abc'
which is stored in the $profile entity object and is passed to the form by $form->setData($profile); So when the data is being set, the error is thrown because of the Numbertype in this line ->add('postalcode', NumberType::class). Is there a way to pass the postalcode value to the form, even if it's not numeric and only check the Numbertype, when the form is submitted? Because i don't need validation, when I pass data to the form. Just when, it's submitted.
The solution was quite simple, but hard to find out.. I changed
->add('postalcode', NumberType::class)
to
->add('postalcode', TextType::class)
in my EditProfileFormType.php. Why? Because the form builder just need to know the type of the field in the database. It's shouldn't care in this case if it is numeric or not, because thats the task of the model restriction. In this case it is a string, so it is Texttype in a form. All the form type's are applied, when the form is set, but the validation groups are just validated, when the form is submitted! That's exactly they way it should be!

Symfony 3: Embedded form whole or nothing validation error display

I have the exactly same problem as described here: Optional embed form in Symfony 2:
I have a form for the entity Person that has an embedded form for the entity Phone. The user can leave all fields of Phone empty and the form will be valid. But if a single field of Phone was filled-in, all Phone-fields must be valid.
During my first approach, I simply annotated the Phone property of Person with #Assert\Valid() without #Assert\NotNull(). That works fine only when entering a new Person. When editing an existing Person and the Phone property was already filled-in, the deletion of all Phone fields (which should be valid) does not result into a valid submit.
The validation of this solution with a validation callback function works with some modifications for Symfony 3:
/**
*
* #Assert\Callback()
*/
public function validatePhone(ExecutionContextInterface $context)
{
if (/* Fields are not empty */)
{
$context->getValidator()->inContext($context)->validate($this->phone);
}
}
But after submitting the form, validation errors for the phone fields are not shown on the page. I can only see them in the debug toolbar.
Maybe, this solution needs to be modified somehow, to let the errors be displayed after form submission?
But maybe even my first approach might work, if it is somehow possible to set the property Phone of an existing Person object to null, if all form fields of Phone have been cleared?
try to use cascade_validation (be carefull removed from symfony3) and error_bubbling in your formType class
->add('phone', 'collection', array(
'type' => 'text',
'allow_add' => true,
'error_bubbling' => false,
'cascade_validation' => true,
));
Found the answer myself:
The modification of the solution from the other post that I made in order try to let it work for Symfony 3.3 needs to be differently:
/**
*
* #Assert\Callback()
*/
public function validatePhone(ExecutionContextInterface $context)
{
if (/* Fields are not empty */)
{
$context->getValidator()->validate($this->phone);
}
}
I needed a solution to an non-class form, what I've done is:
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('status', TextType::class)
->add('invoice', SomeSubForm::class, [
'required' => false
]);
;
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (!isset($data['invoice'])) {
$event->getForm()->add('invoice', HiddenType::class, [
'required' => false
]);
}
});
}
SubForm has NotBlank asserts on properties.

Date after another date in symfony form

I have a form that contain 2 dates: start date(datedebut) and end date(datefin).
I want the end date to be always after the start date. How can i do that?
My form type:
class ReservationType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('datedebut',DateType::class,array(
'widget' => 'choice',
'years' => range(date('Y'), date('Y')+20),
))
->add('datefin',DateType::class,array(
'widget' => 'choice',
'years' => range(date('Y'), date('Y')+20),
))
->add('nbplaces')
;
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Bridge\TravelBundle\Entity\Reservation'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'Bridge_TravelBundle_Reservation';
}
}
You can use a Callback Validator for this. Injected into that callback is a ExecutionContextInterface by which you can access the form, and thus other form params.
Here's an example:
use Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
// …
$builder
->add('start', 'datetime',
'constraints' => [
new Constraints\NotBlank(),
new Constraints\DateTime(),
],
])
->add('stop', 'datetime', [
'constraints' => [
new Constraints\NotBlank(),
new Constraints\DateTime(),
new Constraints\Callback(function($object, ExecutionContextInterface $context) {
$start = $context->getRoot()->getData()['start'];
$stop = $object;
if (is_a($start, \DateTime::class) && is_a($stop, \DateTime::class)) {
if ($stop->format('U') - $start->format('U') < 0) {
$context
->buildViolation('Stop must be after start')
->addViolation();
}
}
}),
],
]);
Usually these kind of tasks are solved by adding validation constraints to check if value of one field is greater then the other. Implement callback validation constraint as stated in the documentation: http://symfony.com/doc/current/reference/constraints/Callback.html You can also create your custom class constraint validator and place validation logic there: http://symfony.com/doc/current/validation/custom_constraint.html
This way whenever a user tries to submit value of datefin which is less than selected value of datedebut he will see a validation error and the form will not be processed.
After that you can always add some javascript code that will filter available dates in datefin field after value in datedebut field is changed.
Also you can use dynamic form modification to render the second date field (and filter its available dates on server side) only if value of the first one is submitted. Check this out: http://symfony.com/doc/current/form/dynamic_form_modification.html

Symfony2 - simplecms : how add extras data?

I've a problem in my form for the entity Page from simplecms
I want to add an item in the array Extras, so i added it in my formtype :
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Cmf\Bundle\SimpleCmsBundle\Doctrine\Phpcr\Page;
class PageType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', 'text', array(
'label' => 'Titre',
'attr' => array('placeholder' => 'Titre complet de la page')
))
->add('name', 'text', array(
'label' => 'Label',
'attr' => array('placeholder' => 'nom-simplifie-de-la-page')
))
->add('body', 'ckeditor')
->add('locale', 'hidden')
->add('publishable')
->add('extras_link','text', array(
'property_path' =>"extras['link']",
));
}
The vars are in class Page (i didnt had to override it) and functions removeExtra() and addExtra() too (necessary to my form alimentation)
/**
* Add a single key - value pair to extras
*
* #param string $key
* #param string $value - if this is not a string it is cast to one
*/
public function addExtra($key, $value)
{
$this->extras[$key] = (string) $value;
}
/**
* Remove a single key - value pair from extras, if it was set.
*
* #param string $key
*/
public function removeExtra($key)
{
if (array_key_exists($key, $this->extras)) {
unset($this->extras[$key]);
}
}
form is working, but when i submit, it find removeExtra() but not addExtra()
"Found the public method "removeExtra()", but did not find a public "addExtra()" on class Symfony\Cmf\Bundle\SimpleCmsBundle\Doctrine\Phpcr\Page"
Somebody already had this problem? or know how to add data in extras?
THX (sorry for my english)
This is a limitation of the form layer unfortunately. The logic that looks for the right method only finds a adder with one parameter. What we ended up doing is using the burgov/key-value-form-bundle : https://github.com/Burgov/KeyValueFormBundle/
This required a PR like this to enabled in SeoBundle: https://github.com/symfony-cmf/SeoBundle/pull/158
from the code in Page::setExtras i think this should already work, though a similar PR to the one on SeoBundle would make it slightly more efficient.

Categories