Symfony2 - validating additional form elements - symfony

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')),
)
));
}
}

Related

Symfony Form - Custom Button Type can't read property

I'm working with Symfony Form in Symfony 5.4 and I need the following:
I have a DTO with some properties. In our application, the default ButtonType has some special handling in the theme-twig (special container with special classes around the button).
Now I need another custom Button-Type to give this new tyoe his own special theme-handling.
I have built the following code for this:
Custom ButtonType-Class:
class FormAddButtonType extends AbstractType
{
public const BLOCK_PREFIX = 'formaddbutton';
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'attr' => [
'class' => 'button blue-button',
],
]);
}
public function getBlockPrefix(): string
{
return self::BLOCK_PREFIX;
}
public function getParent(): string
{
return ButtonType::class;
}
}
Now I add two buttons to my form:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('test_button_1', ButtonType::class, [
'label' => 'Test Button 1',
'attr' => [
'class' => 'button blue-button',
],
'row_attr' => ['class' => 'noborder'],
]);
$builder->add('test_button_2', FormButtonType::class, [
'label' => 'Test Button 2',
'attr' => [
'class' => 'button blue-button',
],
'row_attr' => ['class' => 'noborder'],
]);
}
The first Button will be rendered without any problem. But the second button (my custom button type) will cause the following error:
Can't get a way to read the property "test_button_2" in class "My\Name\Space\Dto\MyDataDto".
Yeah, this class / object doesn't have a property called "test_button_2". But "test_button_1" doesn't exist either and this button works just fine. Manually setting "'mapped' => false" doesn't work either.
If I add my FormAddButtonType inside of my custom Collection Type via POST_SET_DATA-listener, there is no problem. But if I try to use it in the "main form", it won't work.
Can you tell me what I'm doing wrong?
add
"mapped" => false
in the field option, it stands for "this field doesn't exist in the entity".
I think it does work with the default ButtonType as they probably set it on the
$resolver->setDefaults([ /* ... */ ]);
method
doc: https://symfony.com/doc/current/reference/forms/types/form.html#mapped

Symfony5 custom validator - pass multiple submitted fields (no Entity)

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();
}
}
}

Can a Symfony 2 Action be made initially accessible only from another Action?

I struggled to express what I mean in the title of the question! I'll do my best to make more sense here...
Symfony 2.7
I have a Form, which when it is submitted and successfully validated I would like to feed into a second, independent Form, for further user activity. I would like initial values in the second form to be provided by the first, but that second form then to be independent, e.g. it can go through its own separate submission/validation steps.
I do not want:
it to be possible for a user to go straight to the second form
to have to pass values for the second form as querystring parameters
to use Javascript to achieve this
to persist data to a DB in the first form and pick it up in the second
Conceptually, I would like to be able to validate the first form, and then in the Controller to pass the data received to a different Action, which would show the new Form to the user. Further user submissions would then be handled by the new Action. I feel like this should be possible, I'm just not sure how to make it happen! In a way I'd like the second Action to be private, but it does need to be public so that the second form can be submitted. I'd like to be able to pass data to the second Action directly using an object, but I don't want to expose that entry point as a standard Route.
Thanks in advance.
Sorry for any lack of clarity in the question. Here's how I solved it (I'd still be interested in any different/better solutions):
I created a separate FormType (ReportConfirmType) and Action (ConfirmAction) for the second step. ReportConfirmType has the type of Data Class, and essentially all the same fields as the original FormType (ReportType), but with them all marked readonly. The route is very similar. I also created a private method to act as the "glue" between the first and second steps.
When I'm finished with my first step, I then call the private method, passing it the validated data from the first step (which can be used unchanged). This method sets up the second form and returns the second view. The action of the form needs to be changed to that of the second route.
All subsequent submissions will go to the new route, and when the second form validates I can carry out the final activities of the process.
Here's some example code to illustrate further:
ReportType
class ReportType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text')
->add('completedBy', 'text')
->add('comments', 'textarea', ['required' => false])
->add('format', 'choice', ['choices' => ['pdf' => 'PDF', 'word' => 'MS Word'] ])
->add('save', 'submit', ['label' => 'Submit', 'attr' => ['class' => 'btn btn-primary']])
->getForm();
}
...
ReportConfirmType
class ReportConfirmType extends AbstractType{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text', ['attr' => ['readonly' => 'readonly']])
->add('completedBy', 'text', ['attr' => ['readonly' => 'readonly']])
->add('comments', 'textarea', ['required' => false, 'attr' => ['readonly' => 'readonly']])
->add('format', 'choice', ['choices' => ['pdf' => 'PDF', 'word' => 'MS Word'], 'attr' => ['readonly' => 'readonly'] ])
->add('agree', 'checkbox', ['mapped' => false, 'label' => 'I agree', 'constraints' => [new IsTrue()]])
->add('save', 'submit', ['label' => 'Submit', 'attr' => ['class' => 'btn btn-primary']])
->getForm();
}
...
ReportController
class ReportController extends Controller
{
public function indexAction(Request $request, $id)
{
$form = $this->createForm(new ReportType(), new ReportDetails() );
$form->handleRequest($request);
if ($form->isValid()) {
return $this->confirmPseudoAction($id, $form);
}
return $this->render('Bundle:Report:index.html.twig', ['form'=> $form->createView()]);
}
private function confirmPseudoAction($id, \Symfony\Component\Form\Form $form)
{
$action = $this->generateUrl('form_confirm_report', ['id' => $id]);
$confirmForm = $this->createForm(new ReportConfirmType(), $form->getData(), ['action' => $action]);
return $this->render('Bundle:Report:confirm.html.twig', ['form'=> $confirmForm->createView()]);
}
public function confirmAction(Request $request, $id)
{
$form = $this->createForm(new ReportConfirmType(), new ReportDetails() );
$form->handleRequest($request);
if ($form->isValid()) {
return $this->generateReport($id, $form->getData());
}
return $this->render('Bundle:Report:confirm.html.twig', ['form'=> $form->createView()]);
}
...
routing.yml
form_report:
path: /form/{id}/report
defaults: { _controller: Bundle:Report:index }
requirements:
id: \d+
form_confirm_report:
path: /form/{id}/reportConfirm
defaults: { _controller: Bundle:Report:confirm }
requirements:
id: \d+
And this does what I want! There may be an easier way, but I've done it now...
I believe you can use dynamic generation for submitted forms.
It allows you to customize the form specific to the data that was submitted by the user

symfony2 form: how to save entity and how to add more then one entity to the same form?

I have this function in a entitytype class
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//...other controls
->add('types', 'entity', array(
'class' => 'MyApplicationBundle:Type',
'property' => 'type',
'expanded' => false,
'multiple' => true))
->add('save', 'submit');
}
the archive entity has a type property, many to may relation
/**
* #ORM\ManyToMany(targetEntity="Type", mappedBy="archives")
**/
private $types;
the type entity has a archives property on the other side
/**
* #ORM\ManyToMany(targetEntity="Archive", inversedBy="types")
* #ORM\JoinTable(name="types_archives")
**/
private $archives;
the form is correctly displayed with a select multiple control but I'm only able to save in the archive table, not in the types_archives table. Any idea on how to fix?
also, can I add more then one entity to the same type?
thank you
If just one side of relations saved in database try to do following steps:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//...other controls
->add('types', 'entity', array(
'class' => 'MyApplicationBundle:Type',
// This makes form call setter method on related entity
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
'property' => 'type',
'expanded' => false,
'multiple' => true))
->add('save', 'submit');
}
in Archive entity:
public function addType(Type $type){
$this->types[] = $type;
$type->addArchive($this);
}
public function removeType(Type $type){
$this->types->removeElement($type);
$type->setArchive(null);
}
I hope this helps about first part of your question.
For the second part you can use collection type check out following link:
http://symfony.com/doc/current/reference/forms/types/collection.html
Here are some directions that I would give you.
1. To save the related entities, try reading about "cascade persist" if you are using doctrine for example.
2. To have multiple entities on the form, read about "class composition". A composite object which you will set as the form's "data class" will allow you contain multiple entity objects.

Symfony2 FormBuilder with Entity class

I have a form that works well, there is just one issue with it and I'm hoping that I'll get an answer on how to do what I need to do.
<?php
namespace ADS\UserBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Security\Core\SecurityContext;
class UserType extends AbstractType {
private $type;
public function __construct($type) {
$this->type = $type;
}
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('firstName', 'text', array('required' => true))
->add('lastName', 'text', array('required' => true));
$builder->add('email', 'email', array('required' => true));
$builder->add('parentCompany', 'entity', array(
'class' => 'ADSUserBundle:Company',
'expanded' => false,
'empty_value' => 'CHOOSE ONE',
'required' => false,
'property' => 'companyName'
))
->add('enabled', 'choice', array('choices' => array('1' => 'Enabled', '0' => 'Disabled')))
->add('roles', 'entity', array(
'class' => 'ADSUserBundle:Roles',
'required' => true,
'property' => 'displayName',
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\UserBundle\Entity\User'));
}
public function getName() { return 'ads_userbundle_user'; }
}
I have this form, the portion I am looking at is the 'roles' portion... Right now it created a multiple select box ( as I expect it to ), though the value is sequentially ie: 0,1,2,3,4...
What I really need is to figure out how to take this entity, and make the property to be the displayName ( as it is now ) and get the value to be the corresponding internalName This way it'll give me an array like:
array('ROLE_EXAMPLE' => 'EXAMPLE', 'ROLE_EXAMPLE1' => 'EXAMPLE1')
Any ideas how to accomplish this is greatly appreciated.
Kamil Adryjanek is correct, it is going to be much easier if you change it from an entity to a choice field. I've done some testing, both with FOSUserBundle and without the bundle - in both cases I hit some interesting road blocks.
First, I tried to run it through QueryBuilder in a repository, that didn't work out as it should have. The reason being, the fact that you wanted to be returning an array instead of a ORM object causes an error.
So next, I started looking at creating the choice field. All the guides, say to use the fieldname role instead of roles so I tried that, but I then had to duplicate the UserInterface from FOSUserBundle - I didn't want to do that -- so here I am stressed, and trying to figure it out.
Here is what I ended up doing, which works well.
private $normalRoles = array();
then in the __construct I add: $this->normalRoles = $roles;
Here is the builder:
$builder
->add('roles', 'choice', array(
'multiple' => true,
'choices' => $this->normalRoles
))
;
Originally, I left the multiple part out, figuring that it'd at least let me see an option box. I ended up getting an Array to String conversion error. So, adding the 'multiple' => true in, fixes that error.
Then, in my repository I created a function called normalizeRoles
public function normalizeRoles() {
$data = array();
$qb = $this->getEntityManager();
$query = $qb->createQuery(
"SELECT r.internalName, r.displayName FROM AcmeUserBundle:Roles r"
)->getArrayResult();
foreach ($query as $k => $v) {
$data[$v['internalName']] = $v['displayName'];
}
return $data;
}
From here, we just have to make some small edits in the DefaultController of the UserBundle in the newAction and editAction ( both are the same changes )
So, first off is to put into your Controller use Acme/UserBundle/Entity/Roles in order to avoid any errors and be able to get that repository.
Next, right before you create the form you run the normalizeRoles() function
$roles = $em->getRepository('AcmeUserBundle:Roles')->normalizeRoles()
Then, you pass it through the construct via: new UserType($roles)
full line for that would look like this:
$form = $this->createForm(new UserType($roles), $entity, array(
'action' => $this->generateUrl('acmd.user.edit', array(
'id' => $id)
)
));
or for new:
$form = $this->createForm(new UserType($roles), $entity, array(
'action' => $this->generateUrl('acmd.user.new')
)
));
At this point -- You'll have a working system that will allow you to dynamically add roles into a database table, and then associate those with a new or current user.
You can try do it via query_builder attribute:
$builder->add('roles', 'entity', array(
'class' => 'ADSUserBundle:Roles',
'required' => true,
'property' => 'displayName',
'query_builder' => function (RolesRepository $queryBuilder) {
return $queryBuilder->someMethod() // some method in this repository that return correct query to db.
},
));
In this case it would be better to use choice field Type (http://symfony.com/doc/current/reference/forms/types/choice.html) instead of entity and pass some role choices as option to form because entity field Type get entity id as key for choices:
public function buildForm(FormBuilderInterface $builder, array $options) {
...
$builder->add('roles', 'choice', array(
'choices' => $options['role_choices']
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'ADS\UserBundle\Entity\User',
'role_choices' => array()
));
}
Notice: it's recommended to pass variables to form through options parameter, not in constructor.
if I understand your question correctly, you need a data transformers. They help you to show data in form as you want.
Documentation: http://symfony.com/doc/current/cookbook/form/data_transformers.html

Resources