Symfony: Custom subform validation - php

I have form witch uses custom non-mapped subform.
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('subtype', 'my-subtype');
}
}
The subform is consisting of multiple fields and I need to perform additional check on both of them together. The Callback constraint is perfect for the job. However I can not find a way how to add this constraint on the subform as a whole.
So far I have tried to set the Callback in setDefaultOptions() or set it with setAttribute() in buildForm() but the callback is not evaluated.
Currently I am just adding the Callback to one of the fields:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('field1', 'text')
->add(
'field2', 'text',
array(
'constraints' => array(
new Callback(array(
'methods' => array(array($this, 'validateMyType'))
))
)
));
}
public function validateMyType($data, ExecutionContextInterface $context) {
// Validation failed...
$context->addViolationAt('subtype', "mySubtypeViolation");
return;
}
This however prevents me to add the violation on the whole subtype. What ever I use in addViolationAt() the violation is always added to field which hosts the Callback constraint.

I'm surprised you can't add the Callback in setDefaultOptions(), because I just tested this and this works. That's definitely how I would have done it at first.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'constraints' => new Callback([$this, 'test'])
]);
}
public function test($data, ExecutionContextInterface $context)
{
return;
}
And test method was executed (I checked using debugger).

Firstly I had typo in the configuration so the callback method was not triggered. Secondly the error-bubbling was automatically set so the error was added to the whole form. So only what I needed was to manually disable it.
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults([
'error_bubbling' => false, // Automatically set to true for compound forms.
'constraints' =>
array(
new Callback(array(
'methods' => array(array($this, 'validateMyType'))
))
)]);
}
Than the violation is added like for any other callback:
public function validateFacrMembership($data, ExecutionContextInterface $context) {
$context->addViolation("invalidValueMessage");
}

Related

Symfony form doesn't change value by using FormEvents::PRE_SUBMIT

Following problem I have: In Symfony (Version 4.4.22) I created a FormType with a date-field and a checkbox. If the checkbox was checked then the field should get the value of "31.12.9999".
If a requesting form has the value 1 for the field infiniteValidTo, the value of validTo should change from empty to "31.12.9999". (In my case the date field has the value 'null' when the form was submitted.)
So I added an EventListener to the form builder with a pre_submit hook that will add this info before the form is validating.
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('validTo', DateType::class, [
'required' => FALSE,
'format' => 'dd.MM.yyyy'
])
->add('infiniteValidTo', CheckboxType::class, [
'required' => FALSE
])
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if (isset($data['infiniteValidTo']) && $data['infiniteValidTo'] === '1') {
$data['validTo'] = '31.12.9999';
}
$event->setData($data);
});
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => SettingFormModel::class,
'constraints' => [
new Callback([
'callback' => [$this, 'validateFormModel']
])
]
]);
}
/**
* #param SettingFormModel $object
* #param ExecutionContextInterface $context
*/
public function validateFormModel(SettingFormModel $object, ExecutionContextInterface $context): void {
dump($object);
}
Before leaving the listener method the data-array has the correct values (by dumping the variable).
For validating the form in a dynamical way, I defined a callback method for the data object. When the data container arrives the methods, my change of the validTo field is gone. If I change the field into a simple text field it works, but not for a date field.
After debugging a lot of time, I saw that the method mapFormsToData doesn't transform the change into the form object.
Do I made a mistake by configuration or is this a bug in symfony? Has somebody else the same issue with a form?
I found the mistake. The setter of SettingFormModel was not correct. After repairing the Listener works as it should.
You can use a post_submit event
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('validTo', DateType::class, [
'required' => FALSE
])
->add('infiniteValidTo', CheckboxType::class, [
'required' => FALSE
]);
$builder->get('infiniteValidTo')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
if ($event->getForm()->getData()) {
$event->getForm->getParent()->getData()->setValidTo(new \DateTime('9999-12-31'));
}
});
}

Passing data to buildForm() in Symfony 2.8, 3.0 and above

My application currently passes data to my form type using the constructor, as recommended in this answer. However the Symfony 2.8 upgrade guide advises that passing a type instance to the createForm function is deprecated:
Passing type instances to Form::add(), FormBuilder::add() and the
FormFactory::create*() methods is deprecated and will not be supported
anymore in Symfony 3.0. Pass the fully-qualified class name of the
type instead.
Before:
$form = $this->createForm(new MyType());
After:
$form = $this->createForm(MyType::class);
Seeing as I can't pass data through with the fully-qualified class name, is there an alternative?
This broke some of our forms as well. I fixed it by passing the custom data through the options resolver.
In your form type:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->traitChoices = $options['trait_choices'];
$builder
...
->add('figure_type', ChoiceType::class, [
'choices' => $this->traitChoices,
])
...
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'trait_choices' => null,
]);
}
Then when you create the form in your controller, pass it in as an option instead of in the constructor:
$form = $this->createForm(ProfileEditType::class, $profile, [
'trait_choices' => $traitChoices,
]);
Here's how to pass the data to an embedded form for anyone using Symfony 3. First do exactly what #sekl outlined above and then do the following:
In your primary FormType
Pass the var to the embedded form using 'entry_options'
->add('your_embedded_field', CollectionType::class, array(
'entry_type' => YourEntityType::class,
'entry_options' => array(
'var' => $this->var
)))
In your Embedded FormType
Add the option to the optionsResolver
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Yourbundle\Entity\YourEntity',
'var' => null
));
}
Access the variable in your buildForm function. Remember to set this variable before the builder function. In my case I needed to filter options based on a specific ID.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->var = $options['var'];
$builder
->add('your_field', EntityType::class, array(
'class' => 'YourBundle:YourClass',
'query_builder' => function ($er) {
return $er->createQueryBuilder('u')
->join('u.entity', 'up')
->where('up.id = :var')
->setParameter("var", $this->var);
}))
;
}
Here can be used another approach - inject service for retrieve data.
Describe your form as service (cookbook)
Add protected field and constructor to form class
Use injected object for get any data you need
Example:
services:
app.any.manager:
class: AppBundle\Service\AnyManager
form.my.type:
class: AppBundle\Form\MyType
arguments: ["#app.any.manager"]
tags: [ name: form.type ]
<?php
namespace AppBundle\Form;
use AppBundle\Service\AnyManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MyType extends AbstractType {
/**
* #var AnyManager
*/
protected $manager;
/**
* MyType constructor.
* #param AnyManager $manager
*/
public function __construct(AnyManager $manager) {
$this->manager = $manager;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$choices = $this->manager->getSomeData();
$builder
->add('type', ChoiceType::class, [
'choices' => $choices
])
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\MyData'
]);
}
}
In case anyone is using a 'createNamedBuilder' or 'createNamed' functions from form.factory service here's the snippet on how to set and save the data using it. You cannot use the 'data' field (leave that null) and you have to set the passed data/entities as $options value.
I also incorporated #sarahg instructions about using setAllowedTypes() and setRequired() options and it seems to work fine but you first need to define field with setDefined()
Also inside the form if you need the data to be set remember to add it to 'data' field.
In Controller I am using getBlockPrefix as getName was deprecated in 2.8/3.0
Controller:
/*
* #var $builder Symfony\Component\Form\FormBuilderInterface
*/
$formTicket = $this->get('form.factory')->createNamed($tasksPerformedForm->getBlockPrefix(), TaskAddToTicket::class, null, array('ticket'=>$ticket) );
Form:
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefined('ticket');
$resolver->setRequired('ticket');
$resolver->addAllowedTypes('ticket', Ticket::class);
$resolver->setDefaults(array(
'translation_domain'=>'AcmeForm',
'validation_groups'=>array('validation_group_001'),
'tasks' => null,
'ticket' => null,
));
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$this->setTicket($options['ticket']);
//This is required to set data inside the form!
$options['data']['ticket']=$options['ticket'];
$builder
->add('ticket', HiddenType::class, array(
'data_class'=>'acme\TicketBundle\Entity\Ticket',
)
)
...
}

Symfony2 Form builder add entity 'Could not load type entity'

I've created a form type in Symfony that extends the Abstract type, and added the fields using the builder, but no matter what I do it won't work!
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text');
$builder->add('other', 'entity', array(
'data_class' => 'My\App\DefaultBundle\Entity\Other'
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'runSpeeds' => 'My\App\DefaultBundle\Entity\My',
));
}
public function getName()
{
return 'my';
}
}
Could not load type "entity" 500 Internal Server Error -
InvalidArgumentException
"My" Entity has a column which references the id of "Other" using a foreign key constraint. I want my form to basically have a drop down in the form for "My" that displays all the values from the "name" column in the "Other" entity using the Other.id -> My.other_id as reference.
Update
I have an OtherType (Form type) and the following will work:
$builder>add('name', new OtherType(), array(
'data_class' => 'My\App\DefaultBundle\Entity\Other')
)
But this displays the entire entity in the form. I only want one field from the Other entity to display, and in a dropdown with the choices
You didn't provide the required option class.
As mentioned in the documentation of entity Field Type
EDIT:
Moreover you have two syntax issues (";" is missing)
$builder->add('name', 'text')
$builder->add('other', 'entity', array(
'data_class' => 'My\App\DefaultBundle\Entity\Other'
))
Use the required class attribute, as defined in the basic usage http://symfony.com/doc/current/reference/forms/types/entity.html#basic-usage
$builder->add('other', 'entity', array(
'class' => 'DefaultBundle:Other'
))
If your Other class implements a __toString() method you can use that to determine the label. You can also use property for that:
$builder->add('other', 'entity', array(
'class' => 'DefaultBundle:Other',
'property' => 'name',
))
You need to add this:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'My\App\DefaultBundle\Entity\My'
));
}
And change data_class to class in the following lines:
$builder->add('other', 'entity', array(
'data_class' => 'My\App\DefaultBundle\Entity\Other'
));

Issues with form inheritance

I have a PersonType form and then I have LegalPersonType and NaturalPersonType forms and both extends from PersonType since they have a common field on that form (mapped at Entity level). For example, this is the code for NaturalPersonType.php
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Tanane\FrontendBundle\DBAL\Types\CIType;
use Tanane\FrontendBundle\Form\Type\PersonType;
class NaturalPersonType extends PersonType {
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('lives_in_ccs', 'checkbox', array(
'label' => false,
'required' => false,
'value' => 1,
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
public function getName()
{
return 'natural_person';
}
}
Then at SaveFormController/orderAction() I'm doing this:
$order = new Orders();
$orderForm = $this->createForm(new OrdersType(array($type)), $order, array('action' => $this->generateUrl('save_order')));
But any time I try to render the form I get this error:
Neither the property "nat" nor one of the methods "getNat()", "nat()",
"isNat()", "hasNat()", "__get()" exist and have public access in class
"Tanane\FrontendBundle\Entity\Orders".
Relationship are at Entity level, how I fix that error?
Thanks in advance
1st possible solution
Following suggestions from user here I change, in OrderType.php Form my code to this:
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Tanane\FrontendBundle\DBAL\Types\SFType;
class OrdersType extends AbstractType {
/**
* #var string
*/
protected $register_type;
public function __construct($register_type)
{
$this->register_type = $register_type;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
// here goes $builder with default options remove for see less code
if ($this->register_type[0] == "natural")
{
$builder->add('nat', new NaturalPersonType(), array(
'data_class' => 'Tanane\FrontendBundle\Entity\NaturalPerson'
));
}
elseif ($this->register_type[0] == "legal")
{
$builder->add('leg', new LegalPersonType(), array(
'data_class' => 'Tanane\FrontendBundle\Entity\LegalPerson'
));
}
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Tanane\FrontendBundle\Entity\Orders',
'render_fieldset' => FALSE,
'show_legend' => FALSE
));
}
public function getName()
{
return 'orders';
}
}
I've fixed by adding 'mapped' => FALSE on each new FormType I add in OrdersType but I don't know if this is the right. Also, if I'm defining the data_class here, and NaturalType will never be access directly just trough OrdersType should I remove the default options from that form or should I leave them there? How can I fix the problem now? What I'm missing?
This is not a direct answer to your question but maybe could solve some problem before that happens...
I don't remember to have seen it's possible to extend a form like this instead of extend AbstractType, but as explained in the docs, if you have common fields to share between different types of forms you should use the native framework modularity offered by inherit_data.
If you need something more specific (some special methods to execute on some field) you can create a new field type or extend an existing one using AbstractTypeExtension.
EDIT:
I don't know exactly why you are using this approach (that I never used in my projects) but IMO PersonType, NaturalPersonType and LegalPersonType should be only "FormType/FieldType" initialized with inherit_data (and not entities like in your code) that contains the fields related to their use, while OrdersType should be composed with the block of forms needed to the type of person who fills it and with data_class setted on the UNIQUE entity that store the data outputted by the form.

Form-wide error_bubbling in Symfony 2?

This is how i currently activate errors on my forms:
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title', null, array('error_bubbling' => true))
->add('content', null, array('error_bubbling' => true))
;
}
Is there a form-wide version?
No. In general you dont need to make errors bubble to parent form.
If you want to display all errors in one place, you can do this in the template.
If you are using the form types correctly (maybe don't let symfony guess it) then you should get error bubbling by default as seen here:
http://symfony.com/doc/current/reference/forms/types/text.html#error-bubbling
However If you are using a custom form type then you can set the default error_bubbling by default with configureOptions
final class CustomFormType extends AbstractType
{
/** {#inheritdoc} */
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
}
/** {#inheritdoc} */
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setRequired('label');
$resolver->setDefaults([
'error_bubbling' => false,
'compound' => true,
]);
}
}

Categories