We have a simple form:
namespace App\Form;
...
class SimpleForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('field_1', TextType::class, [
'required' => true,
'mapped' => false,
'constraints' => [
new NotBlank()
]
])
->add('field_2', TextType::class, [
'required' => true,
'mapped' => false,
'constraints' => [
new NotBlank()
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new CustomCheck()
]
]);
}
}
If my understanding is right, CustomCheck() can refer to a complex validation over the whole form data (for instance, to validate some combinations of inputs).
My next step is to create the App\Validator\CustomCheck and App\Validator\CustomCheckValidator classes, as per Symfony's manual.
However, I do not know how to pass the submitted field_1 and field_2 data to "new CustomCheck()". Or, how to access all submitted fields from within my custom validator.
I found it is possible if I were using an Entity (Class Constraint Validator, https://symfony.com/doc/current/validation/custom_constraint.html#class-constraint-validator). But I want to know if it's possible without using an Entity.
Okay, so my findings on the matter is that there is no programmatically way to access and pass the form unmapped fields data as arguments at the level of CustomCheck() within:
$resolver->setDefaults([
'constraints' => [
new CustomCheck()
]
]);
In my case, with no mapped Entity and no mapped fields, I found two ways to have a custom validator that can access any form field data:
A custom in-form callback validator:
// custom callback validator
public function CustomCheck($data, ExecutionContextInterface $context){
// $data doesn't contain the unmapped fields, so I need to extract the form data differently
//var_dump($data['field_1']); // this works only for mapped fields (no Entity/DTO needed for this to work, only mapped fields is sufficient)
$field1_data = $context->getRoot()->get('field_1')->getData(); // this works
$field2_data = $context->getRoot()->get('field_2')->getData();
if(...something_not_good...) {
$context
->buildViolation('Custom error here')
->addViolation();
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new Callback([$this, 'CustomCheck'])
]
]);
}
A custom validator where form data needs to be extracted with $this->context:
// form builder
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => [
new CustomCheck()
]
]);
}
// CustomCheck constraint
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
class CustomCheck extends Constraint
{
public string $message = 'Invalid blah blah.';
}
// CustomCheck validator
namespace App\Validator;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class CustomCheckValidator extends ConstraintValidator
{
// $value will always be null, because nothing gets passed in the first argument to this custom validator (no mapped entity, no mapped fields)
/**
* #param mixed $value
*/
public function validate($value, Constraint $constraint)
{
// extract unmapped form fields data manually
$values = [
'field_1' => $this->context->getRoot()->get('field_1')->getData(),
'field_2' => $this->context->getRoot()->get('field_2')->getData()
];
if(...something_not_good...) {
$this->context->buildViolation('Custom error here')->addViolation();
}
}
}
Related
We are using Symfony Forms for our API to validate request data. At the moment we are facing a problem with the CollectionType which is converting the supplied value null to an empty array [].
As it is important for me to differentiate between the user suppling null or an empty array I would like to disable this behavior.
I already tried to set the 'empty_data' to null - unfortunately without success.
This is how the configuration of my field looks like:
$builder->add(
'subjects',
Type\CollectionType::class,
[
'entry_type' => Type\IntegerType::class,
'entry_options' => [
'label' => 'subjects',
'required' => true,
'empty_data' => null,
],
'required' => false,
'allow_add' => true,
'empty_data' => null,
]
);
The form get's handled like this:
$data = $apiRequest->getData();
$form = $this->formFactory->create($formType, $data, ['csrf_protection' => false, 'allow_extra_fields' => true]);
$form->submit($data);
$formData = $form->getData();
The current behavior is:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => [] }
My desired behavior would be:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => null }
After several tries I finally found a solution by creating a From Type Extension in combination with a Data Transformer
By creating this form type extension I'm able to extend the default configuration of the CollectionType FormType. This way I can set a custom build ModelTransformer to handle my desired behavior.
This is my Form Type Extension:
class KeepNullFormTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer(new KeepNullDataTransformer());
}
}
This one needs to be registered with the 'form.type_extension' tag in your service.yml:
PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension:
class: PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension
tags: ['form.type_extension']
Please note that you still use the CollectionType in your FormType and not the KeepNullFormTypeExtension as Symfony takes care about the extending...
In the KeepNullFormTypeExtension you can see that I set a custom model transformer with addModelTransformer which is called KeepNullDataTransformer
The KeepNullDataTransformer is responsible for keeping the input null as the output value - it looks like this:
class KeepNullDataTransformer implements DataTransformerInterface
{
protected $initialInputValue = 'unset';
/**
* {#inheritdoc}
*/
public function transform($data)
{
$this->initialInputValue = $data;
return $data;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->initialInputValue === null) ? null : $data;
}
}
And that's it - this way a supplied input of the type null will stay as null.
More details about this can be found in the linked Symfony documentation:
https://symfony.com/doc/current/form/create_form_type_extension.html
https://symfony.com/doc/2.3/cookbook/form/data_transformers.html
Considering the following architecture
entityA
{
entityB [12]
}
entityB
{
entityC[]
}
entityC
{
name, defaultValue
}
When creating a new object of entityA I want to list every entityC in database to be able to select them and customize defaultValue like following :
CHECKBOX [x] LABEL name1, INPUT defaultValue1
CHECKBOX [ ] LABEL name2, INPUT defaultValue2
CHECKBOX [x] LABEL name3, INPUT defaultValue3
CHECKBOX [x] LABEL name4, INPUT defaultValue4
etc
... the aim is to generate automatically entityC objects according the selection above, in every one of the 12 entityB objects of this new entityA
->add('categories', CollectionType::class, [
"mapped" => false,
'entry_type' => SharedCategoryType::class
])
Using Category (entityC) as an entitytype is not working because I want to expose default value too. Here's SharedCategoryType
class SharedCategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('enabled', CheckboxType::class, [
"mapped" => false
])
->add('name' , TextType::class) //not a label but not important for now
->add('defaultValue' , MoneyType::class);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Category::class,
]);
}
}
With this code the mapping is not working, but I don't know what is missing...
Edit : the first attemps I made was like this :
->add('budgets', EntityType::class, [
"class" => Category::class,
"mapped" => false,
"multiple" => true,
"expanded" => true
}
but then I cannot modify the defaultValue field. It automatically creates the label based on the name (using the __toString method I defined probably), but I can't find how to add defaultValue to the fields exposed
If you create a related entity, you can use the form event eg
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event)
{
/** #var ResultModel $model */
$model = $event->getData();
$form = $event->getForm();
if (!$model->getExecutive()) {
$form->add('executive', EntityType::class, [
'class' => BundesligaExecutive::class,
'placeholder' => 'bundesliga.executive.choose',
'help' => 'bundesliga.executive.help',
]);
}
...
}
Here find more details https://symfony.com/doc/current/form/dynamic_form_modification.html
By the way, it is probably a better idea to use a selectBox instead of checkboxes
Two simple classes form my app model: Money and Product.
As Money app form being reusable, I've decided to create MoneyType extending AbstractType.
// App\Entity\Product
/**
* #ORM\Embedded(class="Money\Money")
*/
private $price;
// App\Form\ProductType
$builder->add('price', MoneyType::class)
// App\Form\Type\MoneyType
class MoneyType extends AbstractType
{
private $transformer;
public function __construct(MoneyToArrayTransformer $transformer)
{
$this->transformer = $transformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('amount', NumberType::class, [
'html5' => true,
'constraints' => [
new NotBlank(),
new PositiveOrZero(),
],
'attr' => [
'min' => '0',
'step' => '0.01',
],
])
->add('currency', ChoiceType::class, [
'choices' => $this->getCurrenciesChoices(),
'constraints' => [
new NotBlank(),
],
]);
$builder->addModelTransformer($this->transformer);
}
public function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefaults([
'data_class' => null
]);
}
...
}
Is it possible to guess the field type without specifying it explicitly for obtaining the following code?
// App\Form\ProductType
$builder->add('price')
Any help is welcome. Thank you in advance.
You can implement a custom TypeGuesser that reads the doctrine metadata and checks if the field is an embeddable of the desired type. This is a basic implementation
namespace App\Form\TypeGuesser;
use App\Form\Type\MoneyType;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Doctrine\ORM\EntityManagerInterface;
class MoneyTypeGuesser implements FormTypeGuesserInterface
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function guessType($class, $property)
{
if (!$metadata = $this->em->getClassMetadata($class)) {
return null;
}
if (
isset($metadata->embeddedClasses[$property]) &&
'Money\Money' == $metadata->embeddedClasses[$property]['class']
) {
return new TypeGuess(MoneyType::class, [], Guess::HIGH_CONFIDENCE);
}
}
// Other interface functions ommited for brevity, you can return null
}
You can see all the interface methods that you need to implement here.
The Form TypeGuesser is mostly based on the annotation #var Money\Money and you should be able to build your down guesser for your own types, see https://symfony.com/doc/current/form/type_guesser.html
Also take a look at https://github.com/symfony/symfony/blob/4.3/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php on how to guess the type by doctrine orm specific types.
You could derive your own app specific guesser with those two examples.
Running into a minor problem on Symfony 2.8. I have a couple of db fields, one is an integer and one is decimal. When I build my form, these fields are dropdowns so I'm using ChoiceType instead of IntegerType or NumberType.
The form actually works fine, the difference internally between the two apparently doesn't cause an issue, and I can pick a value and it properly saves to the db.
The issue now is in a Listener. When certain fields are changed, I need to kick off an extra process, so I have an event listener and am using the getEntityChangeSet() command.
What I'm noticing is that it's reporting back these fields as changed, because it's recognizing a difference between 1000 and "1000" which I can see on a Vardump output:
"baths" => array:2 [▼
0 => 1.5
1 => "1.5"
]
This is causing the listener to always trigger my hook even when the value really hasn't changed. If I change the form type to Integer, that's just a text entry and I lose my dropdown. How do you force a dropdown ChoiceType to treat a number as a number?
In my entity, this is properly defined:
/**
* #var float
*
* #ORM\Column(name="baths", type="decimal", precision=10, scale=1, nullable=true)
*/
private $baths;
In my regular form:
->add('baths', BathsType::class)
which pulls in:
class BathsType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choices' => array_combine(range(1,10,0.5),range(1,10,0.5)),
'label' => 'Bathrooms:',
'required' => false,
'placeholder' => 'N/A',
'choices_as_values' => true,
]);
}
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType';
}
}
You should only pass values to your choices option, they will be indexed by numeric keys used as strings for "hidden" html input values which will do the mapping behind the scene.
Then use choice_label to set the labels (visible values) as the choices casted to string :
class BathsType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choices' => range(1,10,0.5),
'label' => 'Bathrooms:',
'required' => false,
'placeholder' => 'N/A',
'choices_as_values' => true,
'choice_label' => function ($choice) {
return $choice;
},
]);
}
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType';
}
}
In symfony 4 choices_as_values does not exist, so the solution would be the same as Heah answer but without that option:
class BathsType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'choices' => range(1,10,0.5),
'label' => 'Bathrooms:',
'required' => false,
'placeholder' => 'N/A',
'choice_label' => function ($choice) {
return $choice;
},
]);
}
public function getParent()
{
return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType';
}
}
How can I validate additional form fields that do not exist in my entity and are not even related to them?
For example: A user needs to accept the rules so I can add an additional checkbox with mapping set to false but how can I add a constraint which validates this field?
Or even more advanced: A user needs to repeat his e-mail AND password in the form correctly. How can I validate that they're the same?
I want to avoid adding these fields in my entity because it's not related in any way.
I use Symfony 2.3.
One way is to hang constraints directly on the form element. For example:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$notBlank = new NotBlank();
$builder->add('personFirstName', 'text', array('label' => 'AYSO First Name', 'constraints' => $notBlank));
$builder->add('personLastName', 'text', array('label' => 'AYSO Last Name', 'constraints' => $notBlank));
For the repeating stuff, look at the repeated element: http://symfony.com/doc/current/reference/forms/types/repeated.html
Another approach to validation would be to create a wrapper object for you entity. The wrapper object would contain the additional unrelated properties. You could then set your constraints in validation.yml instead of directly on the form.
Finally, you could build a form type just for one property and add the constraints to it:
class EmailFormType extends AbstractType
{
public function getParent() { return 'text'; }
public function getName() { return 'cerad_person_email'; }
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'label' => 'Email',
'attr' => array('size' => 30),
'required' => true,
'constraints' => array(
new Email(array('message' => 'Invalid Email')),
)
));
}
}