Symfony Conditional Form Validation - symfony

I'm working on a Form in Symfony (2.6). The user can select a free product, which will be shipped to the user. The user has to fill in some personal details and his address (obligated). If he wants to specify another delivery address, he checks a checkbox that is not mapped to an Entity, and completes the delivery address. Now, I want to submit the form, and only validate the delivery address fields if the user has checked this checkbox. How can this be done?
The address Fields and the Delivery Address Fields use the same Form Class mapped to the same Entity. I Use a YAML-file for my constraints.
(part of) validation.yml:
AppBundle\Entity\Address:
properties:
street:
- NotBlank: { message: "Please fill in your first name." }
- Length:
min: 3
max: 256
minMessage: "Please fill in your street name."
maxMessage: "Please fill in your street name."
number:
- NotBlank: { message: "Please fill in your house number." }
- Length:
min: 1
max: 10
minMessage: "Please fill in your house number."
maxMessage: "Please fill in your house number."
postCode:
- NotBlank: { message: "Please fill in your postal code." }
- Length:
min: 2
max: 10
minMessage: "Please fill in your postal code."
maxMessage: "Please fill in your postal code."
city:
- NotBlank: { message: "Please fill in your city." }
- Length:
min: 2
max: 256
minMessage: "Please fill in your city."
maxMessage: "Please fill in your city."
- Type:
type: alpha
message: "Please fill in your city."
country:
- NotBlank: { message: "Please select your country." }
- Country: ~
AppBundle\Entity\Product:
properties:
product:
- NotBlank: { message: "Please select your product." }
- Type:
type: integer
message: "Please select your product."
contact:
- Type:
type: AppBundle\Entity\Contact
- Valid: ~
deliveryAddress:
- Type:
type: AppBundle\Entity\Address
- Valid: ~
Product Form Class:
<?php
class ProductFormType extends AbstractType
{
/**
*
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'product';
}
/**
*
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('contact', new ContactFormType()); //CONTACTFORMTYPE also has an AddressFormType for the Address Fields
$builder->add('differentDeliveryAddress', 'checkbox', array( //delivery address is only specific for this Form
'label' => 'Different Shipping Address',
'required' => false,
'mapped' => false
));
$builder->add('deliveryAddress', new AddressFormType());
//specific
$builder->add('product', 'choice', array(
'choices' => array('a'=>'product x','b' => 'product y'),
'required' => true,
'invalid_message' => 'This field is required',
'label' => 'Your Free Product',
));
$builder->add('submit', 'button', array('label' => 'Submit'));
}
}
Finally the getProductFormAction in My Controller
public function getProductFormAction(Request $request)
{
$product = new Product();
$form = $this->get('form.factory')->create(new ProductFormType($product);
$form->handleRequest($request);
if($form->isValid()){
return new Response('Success'); //just to test
}
return $this->render(
'productForm.html.twig',
array(
'pageTitle' => 'Test',
'form' => $form->createView()
)
);
}

This can relatively easily be achieved through groups.
First, add groups to the fields you only want to validate on certain occasions (your delivery address) fields.
street:
- NotBlank:
message: "Please fill in your first name."
groups: [delivery]
- Length:
min: 3
max: 256
minMessage: "Please fill in your street name."
maxMessage: "Please fill in your street name."
groups: [delivery]
Now that these validations are in a specific group, they will not be validated unless explicitly told to do so.
So now, we let the form determine when to validate this group.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => function (FormInterface $form) {
$data = $form->getData();
if ($data->differentDeliveryAddress()) {
return array('Default', 'delivery');
}
return array('Default');
},
));
}
Here the form will always validate the 'Default' group (all validations without any groups set), and will also validate the 'delivery' group when differentDeliveryAddress is set.
Hope this helps

There is also a lot of material in the Symfony docs about dynamic forms and entity/form validation groups that should point you in the right direction; e.g:
How to Dynamically Modify Forms Using Form Events
How to Choose Validation Groups Based on the Submitted Data
This use case seems to come up a lot on SO. I'd suggest doing a quick search for to see if you can unearth a similar question.
Hope this helps :)

Related

Symfony form validation does not work on edit

I have a form with 3 fields as below : storeClient , type and line that belong to Fashion entity.
I basically have the same problem mentioned here :
Symfony Form Validation not working in Edit
But I was surprised when I edited the form and chose the placeholder as option for Client and I filled the line and type fields , I got that for the client field, it DOES display my validation message "Please choose an option" .which is good .
However for the remaining two fields, if line or type are edited in such a way to choose the placeholder option, the application crashed and gives the argumet exception error.
I did a dump; die; inside the controller (you can see it commented below) . And I got the $fashion object like this when I chose the placeholder for all the fields aka for the client,type and line fields :
Fashion object :
storeClient: null
line: Proxies ...\RefLine
___isinitilized___ :true
and all the info of the line that was set initiallly in this object when I first enterede its edit page.
type: Proxies ...\RefType
___isinitilized___ :true
and all the info of the type that was set initiallly in this object when I first enterede its edit page.
so my question is why the validations work on edit for the client field and does not work for line and type ? I do not think that it is related to the fact that it is a choicettype whereas the other two are entitytype. Moreover, I didn't put a "?" in the setter of client. So i don't see why it works for this field and WHY it gave a Null value when I printed the object and it didn't print the initial client value that was already set in the object when I first landed on the edit page although the two other fields hold the values that were already stored in the object initially.
FashionType.php
->add('storeClient', ChoiceType::class,
[
'label' => 'Store Client',
'choices' => $choicesClient,
'choice_value' => function ($value) {
if ($value instanceof Client) {
return $value->getId();
} else {
return $value;
}
},
'placeholder' => 'Choose ..',
'choice_label' => 'diplayLabel',
'attr' => ['class' => "chosen"],
'required' => true,
]
)
->add('type',
EntityType::class,
[
'label' => 'Clothes Type',
'class' => RefType::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('refType')
->orderBy('refType.id', 'ASC');
},
'attr' => ['class' => "chosen"],
'placeholder' => 'Choose..',
'required' => true,
'choice_label' => 'label',
])
->add('line',
EntityType::class,
[
'label' => 'cotation.creation_form.ligne_de_cotation',
'class' => RefLine::class,
'choice_value' => function ($value) {
if ($value instanceof RefLine) {
return $value->getId();
} else {
return $value;
}
},
'query_builder' => function (EntityRepository $er) {
return $er->getShoppingLines();
},
'attr' => ['class' => "chosen"],
'placeholder' => 'Choose..',
'required' => true,
'choice_label' => 'getLabel',
])
IN my controller, this function is called upon submitting the form :
public function validerAction(Request $request, $idFashion)
{
$em = $this->getDoctrine()->getManager();
/** #var Fashion $fashion */
$fashion = ($idFashion === null) ? new Fashion() : $em->getRepository(
'App:Fashion'
)->find($idFashion);
$form = $this->createForm(FashionType::class, $fashion);
// try {
$form->handleRequest($request);
//} catch(\InvalidArgumentException) {
//dump($fashion);die;
// }
if ($form->isSubmitted() && $form->isValid()) {..}
Here are my setters:
/**
* Set line
*
* #param Refline $line
*
* #return Fashion
*/
public function setLine(RefLine $line)
{
$this->line = $line;
return $this;
}
/**
* Set type
*
* #param RefType $type
*
* #return Fashion
*/
public function setType(RefType $type)
{
$this->type = $type;
return $this;
}
/**
* Set storeClient
*
* #param Client $storeClient
* #return Fashion
*/
public function setStoreClient($storeClient)
{
$this->storeClient = $storeClient;
return $this;
}
THe three fields were declared like this :
/**
* #ORM\ManyToOne(targetEntity="App\Entity\RefLine")
* #ORM\JoinColumn(name="line_id", referencedColumnName="id", nullable=false)
*/
private $line;
In EntityType::class field type is by default nullable. If you want to add validation on that then you have to write this
/**
*
* #Assert\NotBlank(message="Please enter Line", groups="groupName")
*/
private $line;
For more details you can read https://symfony.com/doc/current/validation.html
if you are using group name then you should declare in Form
$resolver->setDefaults([
// ...
'validation_groups' => ['Default', 'groupName'],
]);

Render a Checkbox in Twig; Boolean expected error

I'm trying to render a checkbox inside a form but got the next error:
TransformationFailedException in Form.php line 1149: Unable to
transform value for property path "[active]": Expected a Boolean.
Yes, what I send is an integer 0 or 1, but twig get them as string and it has sense but I really can't find how to render the checkbox.
This is the controller:
$form = $form->add('active', 'checkbox', array('label' => 'User active?','required' => false));
And this is the Twig side:
{{ form_widget(form.active, { attr: { 'class': 'form-control' }}) }}
Any idea?
In your form add a model transformer as #Rinat suggested:
$form->add('active', 'checkbox', array('label' => 'User active?','required' => false));
$form->get('active')
->addModelTransformer(new CallbackTransformer(
function ($activeAsString) {
// transform the string to boolean
return (bool)(int)$activeAsString;
},
function ($activeAsBoolean) {
// transform the boolean to string
return (string)(int)$activeAsBoolean;
}
));
More details here: http://symfony.com/doc/current/cookbook/form/data_transformers.html
You should add
/**
* #ORM\Column(type="boolean")
*/
protected $active = false;
to your domain model.

symfony2: multiple entities one form

I have 2 entities:
ADS\LinkBundle\Entity\Link:
type: entity
table: null
repositoryClass: ADS\LinkBundle\Entity\LinkRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
dateAdded:
type: datetime
expirationDate:
type: datetime
nullable: true
designator:
type: string
length: 255
nullable: false
unique: true
slug:
type: string
length: 255
nullable: true
unique: true
manyToOne:
company:
targetEntity: ADS\UserBundle\Entity\Company
inversedBy: link
joinColumn:
name: company_id
referencedColumnName: id
nullable: true
createdBy:
targetEntity: ADS\UserBundle\Entity\User
inversedBy: link
joinColumn:
name: createdBy_id
referencedColumnName: id
domain:
targetEntity: ADS\DomainBundle\Entity\Domain
inversedBy: link
joinColumn:
name: domain_id
referencedColumnNames: id
oneToMany:
paths:
targetEntity: ADS\LinkBundle\Entity\Path
mappedBy: link
cascade: [persist]
lifecycleCallbacks: { }
and
ADS\LinkBundle\Entity\Path:
type: entity
table: null
repositoryClass: ADS\LinkBundle\Entity\PathRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
pathAddress:
type: string
length: 255
pathWeight:
type: string
length: 255
manyToOne:
link:
targetEntity: ADS\LinkBundle\Entity\Link
inversedBy: paths
joinColumn:
name: link_id
referencedColumnName: id
lifecycleCallbacks: { }
I have everything figured out except for the paths portion of the entity. This is for an A/B split test, so each link can have 2 paths. Each path will consist of a web address, and a number ( 0 - 100 )
Here is my form in it's current state:
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PathType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('pathAddress')
->add('pathWeight')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Path'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
and
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LinkType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('designator')
->add('domain', 'entity', array(
'class' => 'ADS\DomainBundle\Entity\Domain',
'property' => 'domainAddress'
))
->add('paths', 'collection', array('type' => new PathType(), 'allow_add' => true))
->add('Submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Link'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
What I need to figure out, is when creating a link, I need to also be able to create the correct path and weight to go with it. The paths won't be in the database before a link is created.
Here is what I have for my controller:
public function newAction(Request $request) {
$entity = new Link();
$form = $this->createForm(new LinkType(), $entity);
if ($request->isMethod('POST')) {
$form->handleRequest($request);
if ($form->isValid()) {
$code = $this->get('ads.default');
$em = $this->getDoctrine()->getManager();
$user = $this->getUser();
$entity->setDateAdded(new \DateTime("now"));
$entity->setCreatedBy($user);
$entity->setSlug($code->generateToken(5));
$entity->setCompany($user->getParentCompany());
$em->persist($entity);
$em->flush();
return new Response(json_encode(array('error' => '0', 'success' => '1')));
}
return new Response(json_encode(array('error' => count($form->getErrors()), 'success' => '0')));
}
return $this->render('ADSLinkBundle:Default:form.html.twig', array(
'entity' => $entity,
'saction' => $this->generateUrl('ads.link.new'),
'form' => $form->createView()
));
}
Thanks to #Onema ( read the comments above ), I've figured this out. By reading the documentation at http://symfony.com/doc/current/cookbook/form/form_collections.html It gave me information I needed to get this done.
First step in doing what I needed to do, was to create a new form type called PathsType.php which houses the fields associated with the Paths Entity
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class PathType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('pathAddress')
->add('pathWeight')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Path'));
}
public function getName() { return 'ads_linkbundle_path'; }
}
Then modifying the LinkType.php to utilize this new form
<?php
namespace ADS\LinkBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class LinkType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('designator')
->add('domain', 'entity', array(
'class' => 'ADS\DomainBundle\Entity\Domain',
'property' => 'domainAddress'
))
->add('paths', 'collection', array(
'type' => new PathType(),
'allow_add' => true,))
->add('Submit', 'submit')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array('data_class' => 'ADS\LinkBundle\Entity\Link'));
}
public function getName() { return 'ads_linkbundle_link'; }
}
The addition of allow_add makes it so that you can add multiple instances of that form.
Within the view, I now utilize the data-prototype attribute. In the documentation, it has the example using a list item - so that's where I started.
<ul class="tags" data-prototype="{{ form_widget(form.paths.vars.prototype)|e }}"></ul>
Then came the jQuery functions ( listed on the documentation link above, simple copy/paste will work )
This got the system working, with 1 small issue and that in my paths entity, I have a relationship to the Link entity but it was not noticing this relationship and had the link_id field as null
To combat this, we edit LinkType.php one more time, and add by_reference = false to the collection definition. We then edit the addPath method inside the entity to look like so:
public function addPath(\ADS\LinkBundle\Entity\Path $paths)
{
$paths->setLink($this);
$this->paths->add($paths);
}
This sets the current link object, as the link the path is associated with.
At this point, the system is working flawlessly. It's creating everything that it needs to, only need to adjust the display a little bit. I personally opted to use a twig macro to modify the html output contained in data-prototype
my macro as it currently sits (incomplete - but working ) which I added to the beginning of my form.html.twig
{% macro path_prototype(paths) %}
<div class="form-group col-md-10">
<div class="col-md-3">
<label class="control-label">Address</label>
</div>
<div class="col-md-9">
{{ form_widget(paths.pathAddress, { 'attr' : { 'class' : 'form-control required' }}) }}
</div>
</div>
{% endmacro %}
In the HTML for the form itself, I removed the list creation, and replaced it with:
<div class="form-group">
{{ form_label(form.paths,'Destination(s)', { 'label_attr' : {'class' : 'col-md-12 control-label align-left text-left' }}) }}
<div class="tags" data-prototype="{{ _self.path_prototype(form.paths.vars.prototype)|e }}">
</div>
</div>
I then modified my javascript to use the div as a starting point instead of the ul in the example.
<script type="text/javascript">
var $collectionHolder;
// setup an "add a tag" link
var $addTagLink = $('Add Another Destination');
var $newLinkLi = $('<div></div>').append($addTagLink);
jQuery(document).ready(function() {
// Get the ul that holds the collection of tags
$collectionHolder = $('div.tags');
// add the "add a tag" anchor and li to the tags ul
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
addTagForm($collectionHolder, $newLinkLi);
$addTagLink.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
addTagForm($collectionHolder, $newLinkLi);
});
});
function addTagForm($collectionHolder, $newLinkLi) {
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
console.log(index);
if (index == 1) {
console.log('something');
$('a.add_tag_link').remove();
}
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = newForm;
$newLinkLi.before($newFormLi);
}
</script>
Being that these paths are destination addresses for an A/B split test within my marketing app, I opted to limit the paths to 2 per link. And with this, I have successfully setup a form to use a collections type.

choice field symfony "Notice: Array to string conversion "

Trying to validate a choice field (multiple checkboxes) Im having this problem:
"Notice: Array to string conversion "
My validation file looks like this one:
Cgboard\AppBundle\Forms\UploadImageEntity:
properties:
image:
...
cgnetworks:
- Choice:
choices: [flickr, tumblr] //<--- this is giving me problems!!!
My form entity class (Im not going to save this to db for now):
class UploadImageEntity {
public $image;
public $cgnetworks;
}
And my form class:
class UploadImageForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image', 'file')
->add('cgnetworks', 'choice', [
'choices' => $this->getCgNetworks(),
'multiple' => TRUE,
'expanded' => TRUE
]
);
}
public function getCgNetworks()
{
return [
'tumblr' => 'Tumblr',
'flickr' => 'Flickr'
];
}
}
Any idea?
perhaps you need to specify multiple in your validation
cgnetworks:
- Choice:
choices: [flickr, tumblr] //<--- this is giving me problems!!!
multiple: true
Check your Entity field getter. If you have something else instead of
public function getValue(){
return $this->value;
}
You can reach this error.
Form builder uses get and set entity methods, that's why you need to return an allowable value.

Validate a non-model attribute

Env: Symfony2.2 + Propel 1.6
I try to validate a 'customer' form which is linked to the model (Customer) to manage a "create account" (form 1) and a "login account" (form 2). The user is required to check the "I accept the terms of agreement" checkbox and this field is not linked to the model. I'm using a global "validation.yml" file to manage validation rules.
I don't know how to validate the checkbox is checked with the validation.yml file.
I've try several technics:
1/ Put a rule in the validation.yml and add getter/setter in the model:
validation.yml:
MyProject\Model\Customer:
properties:
email:
- NotBlank:
groups: [login, create]
message: Champ obligatoire.
- Email:
groups: [login, create]
message: La valeur saisie doit être un email.
cgv:
- Symfony\Component\Validator\Constraints\True:
groups: [login, create]
message: Vous devez accepter les CGV.
constraints:
- Propel\PropelBundle\Validator\Constraints\UniqueObject:
groups: [create]
fields: email
message: Cet email est déjà inscrit.
- Callback:
groups: [login]
methods:
- [MyProject\Model\CustomerQuery, isCustomerEmail]
MyProject\Model\Customer:
class Customer extends BaseCustomer {
private $cgv;
(...)
public function setCgv($cgv) {
$this->cgv = (Boolean) $cgv;
}
public function getCgv() {
return $this->cgv;
}
}
Result: the rule "True" is ok even if the checkbox isn't checked. If I add a "NotBlank" rule, the validation failed in both check/uncheck cases.
2/ Try to add specific validation rules in the "CustomerType" object (like explained in this article)
/*
*
*
* #author
*/
class CustomerLoginType extends AbstractType {
/**
*
* #param \Symfony\Component\Form\FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('email', 'text', array('required' => true));
$builder->add('fill', 'checkbox', array('mapped' => false, 'required' => false, 'data' => true));
$builder->add('cgv', 'checkbox', array('data' => true, 'mapped' => false, 'required' => true, 'validation_groups' => array('login'), 'constraints' => new True(array('message' => 'Vous devez accepter les Conditions Générales de Vente.'))));
}
/**
*
* #return string
*/
public function getName()
{
return 'customer_login';
}
/**
*
* #param \MyProject\FrontBundle\Form\Type\OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'MyProject\Model\Customer',
'validation_groups' => array('login')
));
}
}
Result: Theses rules are not take into account if a "validation.yml" file exists, it could work if I remove the "Customer" entry in the file, but I would prefer to keep it if possible.
3/ As I can't find a "yml" solution, I've finally add a "manual" validation inside the controller like this:
$form_request = $this->getRequest()->get('customer_login');
if (!isset($form_request['cgv'])) {
$form_customer_login->get('cgv')->addError(new \Symfony\Component\Form\FormError('Vous devez accepter les Conditions Générales de Vente.'));
}
Any idea of how I could add my "accept terms" checkbox rule inside the validation.yml file?

Resources