I've created a form which has some fields, whose required status is dependend on the value entered into another field (to be specific, whether the value is > 0).
It is about ordering a textbook and if the quantity is > 0, then the address-related fields must also be entered.
I've added a custom validation callback, which can check whether the count is > 0 and then adds a Violation. But it gets only executed if the field is not empty (since it is not "required").
Is there a way to always validate, but without having to use the required flag on the field?
Below a snippet:
[...]
$builder->add('firstname', 'text', array(
'label' => 'label.firstname',
'required' => false,
'property_path' => 'Order.firstname',
'constraints' => array(
new Callback(array('callback' => array($this, 'validateOrder')))
)
));
[...]
public function validateOrder($data, ExecutionContextInterface $context)
{
/** #var \Symfony\Component\Form\Form $form */
$form = $context->getRoot();
if(
(
(int)$form->get('count_d')->getData() > 0 ||
(int)$form->get('count_f')->getData() > 0 ||
(int)$form->get('count_i')->getData() > 0
) && $data == '') {
$context->addViolation('profile.order.error');
}
}
Wouldn't it be more appropriate to put callback at form directly (instead of field)?
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'constraints' => array(
new Callback(array('callback' => array($this, 'validateOrder')))
)
));
}
This way, the callback will be invoked in any case...
Related
when i submit my form, I have two values to choose from:
WorkPlanningDay and workPlanningHour.
if workplanninghour is specified, we insert a row in the workplanninghour table and vice versa
When I submit a workPlanningHour, I want to prevent doctrine from inserting a workPlanningDay object with null data. I tried to use a $form->remove but without success.
here is my request datas :
{"statuses":[6],"work_planning":1,"work_planning_hour":{"count_jrtt":1,"end_at":"31-12","jrtt":true,"lower_hour_jrtt":35,"nbr_jrtt":24,"start_at":"01-01","upper_hour_jrtt":39},"work_planning_period":0,"working_time_aspect":{"days":["MON","TUE","WED","THU","FRI"],"time":0,"weekly_hours":35}}
here is my form with my EventListener :
class WorkingTimeParametersRESTType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('work_planning', Field\ChoiceType::class, array(
'property_path' => 'workPlanning',
'required' => true,
'choices' => WorkingTimeParameters::getAllowedWorkPlannings(),
))
->add('work_planning_period', Field\ChoiceType::class, array(
'property_path' => 'workPlanningPeriod',
'required' => false,
'choices' => WorkingTimeParameters::getAllowedWorkPlanningPeriods(),
))
->add('working_time_aspect', WorkingTimeAspectType::class, array(
'property_path' => 'workingTimeAspect',
'required' => true,
'constraints' => array(new Valid())
))
->add('work_planning_hour', WorkPlanningHourType::class, array(
'property_path' => 'workPlanningHour',
'required' => true,
'constraints' => array(new Valid())
))
->add('work_planning_day', WorkPlanningDayType::class, array(
'property_path' => 'workPlanningDay',
'required' => true,
'constraints' => array(new Valid())
))
->add('statuses', Field\CollectionType::class, array(
'entry_type' => CategoryIntegerType::class,
'entry_options' => array(
'category_type' => Category::TYPE_STATUS,
),
'allow_add' => true,
'allow_delete' => true,
'required' => true,
'error_bubbling' => false,
))
->addEventListener(FormEvents::PRE_SET_DATA, array($this, 'onPreSetData'))
->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) {
/** #var WorkingTimeParameters $contract */
$parameters = $event->getData();
$form = $event->getForm();
// dd($form->has('work_planning_day'));
// Remove useless work planning
if ($parameters->getWorkPlanning() == Contract::WORK_PLANNING_HOUR) {
$parameters->setWorkPlanningDay(null);
$form->remove('work_planning_day');
}
else if ($parameters->getWorkPlanning() == Contract::WORK_PLANNING_DAY) {
$parameters->setWorkPlanningHour(null);
$form->remove('work_planning_hour');
}
else {
dd('rerer');
$form->remove('work_planning_day');
$form->remove('work_planning_hour');
$parameters->setWorkPlanningDay(null);
$parameters->setWorkPlanningHour(null);
$parameters->setWorkPlanningPeriod(null);
}
})
;
}
/**
* #param FormEvent $event
*/
public function onPreSetData(FormEvent $event)
{
$rttParameters = $event->getData();
$form = $event->getForm();
// The company can be set only on creation
if (!$rttParameters || !$rttParameters->getId()) {
$form->add('company', CompanyIntegerType::class, array(
'required' => true,
));
}
}
and here are the requests that doctrine is trying to launch :
INSERT INTO w2d_tm_work_planning_hour (startAt, endAt, jrtt, countJrtt, lowerHourJrtt, upperHourJrtt, nbrJrtt) VALUES ('1970-01-01 00:00:00', '1970-12-31 00:00:00', 1, 1, 35, 39, 24);
UPDATE w2d_tm_work_planning_day SET startAt = NULL, endAt = NULL, nbrDays = NULL, nbrJrtt = NULL WHERE id = 1;
how to do so that doctrine does not launch the second request with the null values ?
Symfony form type are not made to handle different entities depending on a option you selected inside.
This not how Symfony FormType are supposed to be used. This is why the data_class attribute is made for one entity.
You best option is to create two distinct FormType and submit only the one needed depending on what the user selected on frontend. (easy with a little of javascript)
The second advantage of this is you will have much more understandable and maintable code.
I'm using Symfony 5.1 to filter an entity in a form type.
->add('ageGroup', EntityType::class, [
'class' => AgeGroup::class,
'choice_label' => 'name',
'choice_filter' => ChoiceList::filter(
$this,
function ($ageGroup) {
if ($ageGroup instanceof AgeGroup) {
return $ageGroup->getOrganization()->getId() == $this->security->getUser()->getOrganization()->getId();
}
return false;
}
)
])
->add('room', EntityType::class, [
'class' => Room::class,
'choice_label' => 'name',
'choice_filter' => ChoiceList::filter(
$this,
function ($room) {
if ($room instanceof Room) {
return $room->getBranch()->getOrganization()->getId() == $this->security->getUser()->getOrganization()->getId();
}
return false;
}
),
])
I have an interesting issue. Whichever ChoiceList::filter is first will work but no other filter will work after.
This is when ->add('ageGroup') was placed first.
This is when ->add('room') was placed first.
The 2nd choice list is not called. At all.
In fact, no other ChoiceList::filter is called if I add more than 1.
https://symfony.com/doc/current/reference/forms/types/choice.html places no limitation on how many can be called.
Clearly I'm doing so
I can't even begin to guess what's wrong with it.
The ChoiceList::filter method takes a third argument named $vary.
As per the PHPDoc comment:
/**
* #param FormTypeInterface|FormTypeExtensionInterface $formType A form type or type extension configuring a cacheable choice list
* #param mixed $option Any pseudo callable, array, string or bool to define a choice list option
* #param mixed|null $vary Dynamic data used to compute a unique hash when caching the option
*/
The hashing function used within this method uses php's built-in spl_object_hash() on the $formType object, which will yield the same id for both callbacks. Pass the field name (or anything else that is unique) as a third argument:
->add('ageGroup', EntityType::class, [
'class' => AgeGroup::class,
'choice_label' => 'name',
'choice_filter' => ChoiceList::filter(
$this,
function ($ageGroup) {
if ($ageGroup instanceof AgeGroup) {
return $ageGroup->getOrganization()->getId() == $this->security->getUser()->getOrganization()->getId();
}
return false;
},
'ageGroup'
)
])
->add('room', EntityType::class, [
'class' => Room::class,
'choice_label' => 'name',
'choice_filter' => ChoiceList::filter(
$this,
function ($room) {
if ($room instanceof Room) {
return $room->getBranch()->getOrganization()->getId() == $this->security->getUser()->getOrganization()->getId();
}
return false;
},
'room'
),
])
I'm trying to create an address form with a payment and shipping address. When a checkbox on the shipping address is checked, I want to skip form validation from occurring on that address.
I have created the form type below with a toggle option that will display and deal with the checkbox, however the form is still validated even when checked.
Symfony has documentation on how to implement such a form, and even though I almost have the exact same code, validation is not turned off when checked. I am not using validation groups, so I just disable the default group to disable validation on the entity.
The AddressType building a form for the Address class (which has annotation constraints on certain fields like NotBlank and Callback).
class AddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($options["toggle"]) {
$builder->add("toggle", CheckboxType::class, [
"mapped" => false,
"required" => false,
"label" => $options["toggle"]
]);
}
$builder
->add("name", TextType::class, [
"required" => !$options["toggle"]
])
->add("address", TextType::class, [
"required" => !$options["toggle"]
])
->add("zipcode", TextType::class, [
"label" => "Postcode",
"required" => !$options["toggle"]
])
->add("city", TextType::class, [
"required" => !$options["toggle"]
])
->add("countryCode", ChoiceType::class, [
"choices" => Address::COUNTRY_CODES
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
"toggle" => false,
"data_class" => Address::class,
"validation_groups" => function(FormInterface $form) {
if ($form->has("toggle") && $form->get("toggle")->getData() === true) {
return [];
}
return ["Default"];
}
]);
$resolver->setAllowedTypes("toggle", ["bool", "string"]);
}
}
I use the type like this:
$addressForm = $this
->createFormBuilder([
"paymentAddress" => $paymentAddress,
"shippingAddress" => $shippingAddress
])
->add("paymentAddress", AddressType::class, [
"label" => false
])
->add("shippingAddress", AddressType::class, [
"label" => false,
"toggle" => "Use payment address"
])
->add("submit", SubmitType::class, [
])
->getForm();
I've been going over this for a few hours now but I am not able to deduce why validation is not being turned off, and I am not willing to botch up a form over this little detail.
Why is validation for the AddressType not turned off by the closure in configureOptions? If this is just not how it works, what would be a better solution to accomplish partially turning off validation in a tidy way?
EDIT: Even if setting "validation_groups" => false in the defaults, in the children created in the builder, or in the usage of the form, validation will still happen. It does not have to do with the closure. Every online resource, including Symfony's own resource states that it should work though...
[...] so I just disable the default group to disable validation on the entity.
Assuming that Address properties & constraints look like this:
/**
* #Assert\NotBlank()
*/
private $name;
// etc.
The "validator" assumes that these properties will be evaluated with Default group, because it always considers empty validation_groups (e.g. return [];) as ['Default'] (hence validation is not turned off when checked):
https://symfony.com/doc/current/validation/groups.html:
If no groups are specified, all constraints that belong to the group Default will be applied.
A solution to accomplish partially turning off validation in a tidy way
Should there be many ways to achieve it, but I show you two of them:
If none data_class is set to the root form, then Form group is available to validate this level only:
$addressForm = $this
->createFormBuilder([...], [
'validation_groups' => 'Form', // <--- set
])
Next, in configureOptions method, set Address group as default and Form group if "toggle" is checked, also adds Valid() constraint for cascade validation:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ...
"validation_groups" => function(FormInterface $form) {
if ($form->has("toggle") && $form->get("toggle")->getData() === true) {
return ['Form']; // <--- set
}
return ['Address']; // <--- set
},
'constraints' => [new Assert\Valid()], // <--- set
]);
}
That means, on submit with toggle off: Form and Address groups are applied to address fields, else, only Form group is applied.
(Another way) In Address class add "Required" group to all constraints, it avoids validate these properties with Default group:
/**
* #Assert\NotBlank(groups={"Required"}) // <--- set
*/
private $name;
// etc.
Next, in configureOptions method set Required as default group and Default if toggle is checked:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
// ...
"validation_groups" => function(FormInterface $form) {
if ($form->has("toggle") && $form->get("toggle")->getData() === true) {
return ['Default']; // <--- set
}
return ['Required']; // <--- set
},
'constraints' => [new Assert\Valid()], // <--- set
]);
}
In this case, on submit with toggle off: Default and Required groups are applied to address fields, else only Default group, skipping thus the required fields.
Nested forms that contain objects disconnected from the root object can be validated by setting the constraints option to new Valid().
[Documentation about collection] When embedding forms (collection type) is possible to specify validation groups for each item, based on the current item? It seems not working ATM.
The TaskType form adding a collection of tags:
// src/Acme/TaskBundle/Form/Type/TaskType.php
// ...
public function buildForm(FormBuilderInterface $builder, array $options)
{
// ...
$builder->add('tags', 'collection', array(
// ...
'by_reference' => false,
));
}
For example we have two tags (tag 1 and tag 2) and a new tag is added using the "Add" button (via JavaScript):
-----------
| add tag |
-----------
- tag 1 (existing)
- tag 2 (added clicking the "add tag" button)
Tag 1 should be validated against Default, Edit groups while tag 2 against Default group only.
TagType form defining dynamic validation groups
Based on the underlying data, if tag is new it gets Default group, if exists Default, Create groups:
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$tag = $form->getData();
$groups = array('Default');
if (null !== $tag && null !== $tag->getId()) {
$groups[] = 'Edit';
}
return $groups;
}
));
}
// ...
Tag entity with a constraint in the "Edit" group
An example with Tag defining two properties (accessors omitted):
class Tag
{
/**
* #Assert\NotBlank()
*/
protected $name;
/**
* #Assert\NotBlank(groups={"Edit"})
* #Assert\Length(max="255")
*/
protected $description;
// ...
}
For an existing tag: description should not be blank. For a new tag: description can be blank.
Proof form is valid, validator shows errors (wrong!)
Just edit an existing tag and leave the description blank. The form validates but the validator service shows errors:
$form = $this->createForm('task', $task)
->handleRequest($request);
$validator = $this->get('validator');
if ($form->isValid()) {
foreach ($task->getTags() as $tag) {
// Validate against Default, Edit groups
$errors = $validator->validate($tag, array('Default', 'Edit'));
if (null !== $tag && null !== $tag->getId()) {
echo 'Existing tag #'.$tag->getId();
} else {
echo 'New tag';
}
echo ', errors: '.count($errors).'<br>';
}
die('Form is valid.')
// ...
}
Output:
Existing tag #863, errors: 1
Form is valid.
Update 1: I've tried (without success) with a static method determineValidationGroups as suggested here:
public static function determineValidationGroups(FormInterface $form)
{
$groups = array('Default');
if ($form->getData() !== null && null !== $form->getData()->getId())
{
$groups = array('Edit');
}
var_dump($groups);
return $groups;
}
In TagType form:
/**
* {#inheritdoc}
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
// ...
'validation_groups' => array(
'Acme\TaskBundle\Entity\Tag',
'determineValidationGroups'
),
));
}
Output with just one existing tag and one created using the "add tag" link seems correct. But no errors for the existing tag leaving the description blank:
array (size=1)
0 => string 'Edit' (length=4)
array (size=1)
0 => string 'Edit' (length=4)
rray (size=1)
0 => string 'Default' (length=7)
rray (size=1)
0 => string 'Default' (length=7)
The complete code I used to test my answer is on https://github.com/guilro/SymfonyTests/tree/SO21276662.
Valid constraint force Validator to validate embed object, and AFAIK the API provides no way to set validation group.
But at a higher level, we can ask Form component to cascade ValidationListener to all embed forms, and use the Form component API to set validation group.
We must use :
'cascade_validation' => true option in the FormBuilder, at all levels. It is set to false by default.
a callback in TagType settings to set validation group. (You were on the right track.)
'error_bubbling' => false, as it is true by default in collections
and we're done, we can display the form with all errors next to corresponding fields.
in TaskType.php :
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('tags', 'collection', array(
'type' => 'tag',
'error_bubbling' => false,
'allow_add' => true,
'by_reference' => false,
'cascade_validation' => true
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Task',
'cascade_validation' => true
));
}
}
in TagType.php :
class TagType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description', 'text', array('required' => false));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\TaskBundle\Entity\Tag',
'validation_groups' => function(FormInterface $form) {
if ($form->getData() !== null && null !== $form->getData()->getId())
{
return array('Edit');
}
return array('Default');
},
'error_bubbling' => false,
));
}
}
I have got a simple smyfony2 form with one choices element. When I choose "kerosin" or "diesel" the form won't validate, what is correct. When I won't choose any of the three options and submit the form empty, $form->validate() will return true, but it shouldn't. Any ideas? Using the HTML5 required is not a solution for me.
This is my Form AbstractType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Form erzeugen
$builder->add('treibstoff', 'choice', array(
'choices' => array(
'kerosin' => 'form.quiz1.kerosin',
'benzin' => 'form.quiz1.benzin',
'diesel' => 'form.quiz1.diesel',
),
'multiple' => false,
'expanded' => true,
'label' => ' '
))
->getForm();
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// Validierung erzeugen
$collectionConstraint = new Collection(array(
'treibstoff' => array(
new Choice(array(
'choices' => array('benzin'),
'message' => 'form.quiz.falscheantwort',
'strict' => true
)
)
)
));
$resolver->setDefaults(array(
'validation_constraint' => $collectionConstraint
));
}
public function getName()
{
...
Validation works like this:
if($Request->getMethod() == "POST") {
$form->bind($Request);
if($form->isValid()) {
echo "valid";
Thanks in advance.
Edit:
I changed the setDefaultOptions like suggested and added NotBlank. That worked out for me:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// Validierung erzeugen
$collectionConstraint = new Collection(array(
'treibstoff' => array(
new Choice(array(
'choices' => array('benzin'),
'message' => 'form.quiz.falscheantwort',
'strict' => true,
)
),
new NotBlank()
)
));
$resolver->setDefaults(array(
'validation_constraint' => $collectionConstraint
));
}
You set only valid choice to benzin in setDefaultOptions, but you didn't specify the field as required. Note that required in form field only sets HTML5 validation on:
Also note that setting the required option to true will not result in
server-side validation to be applied. In other words, if a user
submits a blank value for the field (either with an old browser or web
service, for example), it will be accepted as a valid value unless you
use Symfony's NotBlank or NotNull validation constraint.
So, you'll have to add also NotBlank constraint to treibstoff field.