Symfony Validation: Error message not showing at associated field - forms

Symfony 5.2.5
Minified code
//Entities
class Article {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\OneToMany(targetEntity=ArticleTranslation::class, mappedBy="article", cascade={"persist"}, orphanRemoval=true)
* #Assert\Valid
*/
private $translations;
}
class ArticleTranslation {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
* #Assert\NotBlank
*/
private $title;
/**
* #ORM\Column(type="text")
* #Assert\NotBlank
*/
private $body;
/**
* #ORM\ManyToOne(targetEntity=Article::class, inversedBy="translations")
* #ORM\JoinColumn(nullable=false)
*/
private $article;
/**
* #ORM\Column(type="string", length=5)
* #Assert\NotBlank
*/
private $locale;
}
//FormTypes
class ArticleType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(/*other fields*/)
->add('translations', ArticleTranslationType::class, ['label' => false, 'data' => new ArticleTranslation(), 'mapped' => false])
->add('save', SubmitType::class, ['label' => 'Save']);
$builder->get('translations')->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
$entity = $event->getForm()->getParent()->getData();
$translation = $event->getData();
$translation->setLocale($this->localeService->getCurrentLocale()); //custom service returns e.g. "en"
$entity->addTranslation($translation);
});
}
}
class ArticleTranslationType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class)
->add('body', TextareaType::class);
}
}
I have two entities called Article and ArticleTranslation with a OneToMany relationship. When creating an Article I want to add a ArticleTranslation to it (e.g. English) - that way there is atleast 1 translation provided. The Article itself just stores generic data like publish-date, while the translation stores title and the content (called body). The above code works fine my only issue is following:
When the validation for title or body fails, the error message is shown above the formular, instead of right next to the associated field. Every other field correctly has the error message right next to it. I am using the default bootstrap 4 form theme.
How can I move the error message to the correct field? The Symfony profiler returns that data.translations[0].body should not be null (since its a collection it has an index) - I guess I need somehow make that into data.translations.body for it to work?
Temporary fix: When adding the validation inside my ArticleTranslationType & remove the Assert\Valid constraint it works. Still interested in another solution with my provided code - Thanks

What you're looking for is the error_bubbling FormType field option.
error_bubbling
type: boolean default: false unless the form is compound.
If true, any errors for this field will be passed to the parent field or form. For example, if set to true on a normal field, any errors for that field will be attached to the main form, not to the specific field.
Your ArticleTranslationType is compound, therefore error_bubbling defaults to true.
The following should do the trick.
$builder->add(
'translations', ArticleTranslationType::class, array(
'data' => new ArticleTranslation(),
'error_bubbling' => false,
'mapped' => false,
'label' => false
)
);

After playing around I finally got my solution. Since the validation tries to validate the first element in my collection e.g. data.translations[0].body I needed just to provide the correct property path for it to know.
$builder->add(
'translations', ArticleTranslationType::class, array(
'data' => new ArticleTranslation(),
'mapped' => false,
'label' => false,
'property_path' => 'translations[0]' //first element of collection
)
);
This maps the error messages to the corresponding field.

Related

Symfony 4.3 dynamic form for tags solution with Doctrine many to one association

First, I'm sorry for my bad english. I need to create form to adding new tags for Article but when I submit form then Request data is not handled in my form because new added tags are not in entity array collection. Is possible to add custom choices to form field with many to one association?
Here is my code:
public function buildForm(FormBuilderInterface $builder, array $options)
{
dump($builder->getFormConfig()); die;
/** #var Domain $domain */
$domain = $this->currentDomainService->getCurrentDomain();
$builder
->add('articleTitle', TextType::class, [])
->addEventSubscriber(new TagsChoicesSubscriber())
;
}
class TagshoicesSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => ['preSetData', -50],
FormEvents::PRE_SUBMIT => ['preSetData', -50],
);
}
public function preSetData(FormEvent $event, $childName)
{
$choices = array();
/** #var Article $article */
$article = $event->getData();
if ($article instanceof Article) {
foreach ($article->getTags() as $tag) {
$tags[] = $tag->getTagName();
}
$event->getForm()->add(
'tags',
ChoiceType::class,
[
'multiple' => true,
'mapped' => false,
'choices' => $choices,
'data' => $tags,
'required' => true,
'constraints' => [
new NotBlank(),
],
]
);
}
}
}
/**
* Article
* #ORM\Entity()
*/
class Article
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Tags", mappedBy="article")
*/
private $tags;
}
/**
* Tag
*
* #ORM\Entity()
*/
class Tag
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Article", inversedBy="tags")
* #ORM\JoinColumn(name="article_id", referencedColumnName="id")
*/
private $article;
}
$form = $this->createForm('App\Form\ArticleType', $article);
$form->handleRequest($request);
The thing you need to implement is either collection type field, or choice type with multiple set to true, here is Symfony collection type and here is Symfony choice type, you will also need a toString function in Tag class.

Create a table form from Doctrine entities

I have a list of Doctrine entities (called "Circuit") and would like to generate a form listing them within a <table> and add a way to tick them for mass deletion (kind of what Sonata Admin does, without the need for an admin class).
I've looked everywhere but I can't figure out for the life of me what to do. There is just one layer to this class (plain old object), and every time I try to add a collection type to my form builder I get the following error:
Neither the property "circuits" nor one of the methods "getCircuits()", "circuits()", "isCircuits()", "hasCircuits()", "__get()", "__call()" exist and have public access in class "NetDev\CoreBundle\Entity\Circuit".
Am I supposed to create a "proxy" class to create a collection of circuits ? Did I miss something ?
All the howtos I found so far are using a "master" class like "Article" and a collection of child classes like "Categories" which doesn't apply to my present issue.
Here is my CircuitsController.php (I use the "addAction" for the tests, eventually everything will be located in indexAction):
<?php
namespace NetDev\WebManagerBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use NetDev\CoreBundle\Form\CircuitType;
use NetDev\CoreBundle\Entity\Circuit;
class CircuitsController extends Controller {
public function indexAction($page = 1) {
$listCircuits = $this->getDoctrine()->getManager()->getRepository('NetDevCoreBundle:Circuit')->findAll();
$content = $this->get('templating')->render('NetDevWebManagerBundle:Circuits:index.html.twig',
array('listCircuits' => $listCircuits));
return new Response($content);
}
public function addAction(Request $request) {
$circuit = new Circuit();
$form = $this->createForm(new CircuitType(), $circuit);
if ($request->isMethod('POST') && $form->handleRequest($request)->isValid()) {
/* some action that is not actually relevant */
}
return new Response($this->get('templating')->render('NetDevWebManagerBundle:Circuits:add.html.twig',
array('circuit' => $circuit,
'form' => $form->createView())));
}
The CircuitType.php file:
<?php
namespace NetDev\CoreBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CircuitType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('circuits', 'collection', array('type' => 'entity', 'allow_add' => true,
'allow_delete' => true, 'by_reference' => false,
'label' => false,
'options' => array('class' => 'NetDevCoreBundle:Circuit',
'label' => false, 'multiple' => true,
'expanded' => true)
))
/* ->add('vlanId', 'integer', array('required' => true, 'label' => 'VLAN ID')) */
/* ->add('popOut', 'text', array('required' => true, 'label' => 'Injecting PoP', */
/* 'max_length' => 3)) */
/* ->add('popsIn', 'textarea', array('required' => true, 'label' => 'Listening PoP')) */
/* ->add('bandwidth', 'integer', array('label' => 'Bandwidth')) */
/* ->add('xconnectId', 'text', array('label' => 'Cross-connect ID')) */
/* ->add('Create', 'submit') */
;
}
/**
* #return string
*/
public function getName()
{
return 'netdev_corebundle_circuit';
}
}
And finally, the Circuit.php entity file:
<?php
namespace NetDev\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* Circuit
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="NetDev\CoreBundle\Entity\CircuitRepository")
*/
class Circuit
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
*
* #ORM\Column(name="vlan_id", type="integer")
* #Assert\Type(type="int")
* #Assert\Range(min="1", max="4096")
*/
private $vlanId;
/**
* #var array
*
* #ORM\Column(name="pop_out", type="array")
* #Assert\NotBlank()
* #Assert\Length(max=3)
*/
private $popOut;
/**
* #var array
*
* #ORM\Column(name="pops_in", type="array")
* #Assert\NotBlank()
*/
private $popsIn;
/**
* #var integer
*
* #ORM\Column(name="bandwidth", type="integer")
* #Assert\Type(type="int")
*/
private $bandwidth;
/**
* #var string
*
* #ORM\Column(name="xconnect_id", type="string", length=255)
* #Assert\NotBlank()
* #Assert\Length(max="255")
*/
private $xconnectId;
/* Getters and Setters stripped for clarity's sake */
public function __toString() {
return "{$this->vlanId}-{$this->popOut}";
}
}
If you need the twig template tell me, I haven't added it because I am not even close to having something outputted aside from that Exception.
As said by #Cerad, the answer here was to pass the result of
$listCircuits = $this->getDoctrine()->getManager()->getRepository('NetDevCoreBundle:Circuit')->findAll();
directly to the collection. Everything worked nicely afterwards.

Symfony2 Custom Form Type or Extension

An Entity Order exists with a property Product.
A form has been created OrderType that allows a Product to be added to an Order.
This works, however it's not very interesting.
Instead of showing a simple Product drop down, it should be an autocomplete.
However when choosing an autocomplete value some additonal fields should be populated with information about the product.
Choosing a product from the autocomplete should populate two additional fields with Price and Code.
The controller method to return the data has aleady been created and jquery have some handy autocomplete functions avaiable.
I know how to hack the solution directly into the form template but I would like to make a reusable component.
The question is how do I create a custom form or extension with this behaviour?
class Order {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="orders", cascade={"persist"})
* #ORM\JoinColumn(name="product_id", referencedColumnName="id")
*/
protected $product;
protected $quantity;
}
class Product {
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="product")
*/
protected $orders;
protected $name;
protected $price;
protected $code;
}
class OrderType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('quantity')
->add('product');
}
}
Update
I have updated OrderType with the following:
$builder
->add('ppprice', 'text', array('mapped' => false, 'data' => 2));
$builder->addEventListener(
FormEvents::PRE_SET_DATA, function (FormEvent $event) use($builder) {
$form = $event->getForm();
$order = $event->getData();
$builder
->add('ppprice', 'text', array('mapped' => false, 'data' => 21));
$builder
->add('test', 'text', array('mapped' => false, 'data' => 21));
}
);
PRE_SET_DATA is being called but the new form field test is never added and ppprice is not updated with the new value.
How do I get PRE_SET_DATA to update the value?
You should attach a FormEvent that handles the POST_SUBMIT event on your forms.
There's a tutorial with a full example (incl. jquery ajax behavior) here:
http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html
This will take care of adding dynamic fields to your FormType. You can create an EventListener if you are looking to re-use this later, or you can attach the event via Closures when building your custom form type.
Update:
Try the following code first to see and check if the PRE_SET_DATA event properly kicks in. Please note that you can not inject $builder into this Closure. (You can but won't work) Simply use the $form object and ->add fields in the Closure like this:
$builder->addEventListener(
FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$order = $event->getData();
$form->add('ppprice', 'text', array('mapped' => false, 'data' => 21));
$form->add('test', 'text', array('mapped' => false, 'data' => 21));
}
);

Symfony2 validation groups and mapping

For now I've successful used validation groups, but now I'm stuck with validation groups and nested mapped entities.
I'll explain the problem by a simplified example.
My entities: Address, Damage, Appliance
/**
* #ORM\Entity()
*/
class Address extends ...
{
/**
* #var string
* #ORM\Column(type="string", name="postcode", nullable=true)
* #Assert\NotBlank(
* groups={
* "damage_responsible_address",
* "appliance_repairer_address",
* })
*/
private $postcode;
...
/**
* #ORM\Entity()
*/
class Damage extends ...
{
/**
* #var boolean
* #ORM\Column(type="boolean", name="responsible", nullable=true)
* #Assert\NotBlank(groups={"damage"})
*/
private $responsible;
/**
* #ORM\OneToOne(targetEntity="Address", cascade={"persist","remove"})
* #ORM\JoinColumn(name="responsible_address_id", referencedColumnName="id")
* #Assert\Valid()
*/
private $responsibleAddress;
/**
* #ORM\ManyToMany(targetEntity="Appliance", orphanRemoval=true, cascade={"persist", "remove"})
* #ORM\JoinTable(name="coronadirect_cuzo_home_damage_appliances",
* joinColumns={#ORM\JoinColumn(name="damage_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="appliance_id", referencedColumnName="id")}
* )
*/
private $appliances;
...
/**
* #ORM\Entity()
*/
class Appliance extends ...
{
/**
* #var boolean
* #ORM\Column(type="boolean", name="to_repair", nullable=true)
* #Assert\NotBlank(groups={"appliance"})
*/
private $toRepair;
/**
* #ORM\OneToOne(targetEntity="Address", cascade={"persist","remove"})
* #ORM\JoinColumn(name="repairer_address_id", referencedColumnName="id")
* #Assert\Valid()
*/
private $repairAddress;
...
To define my forms I use a AddressType, DamageType and ApplianceType:
class DamageType extends ...
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('appliances', 'collection', array(
'type' => 'home_damage_appliance_type',
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'options' => array(
'cascade_validation' => true,
)
));
$builder->add('responsible', 'choice', array(
'choices' => $this->getYesNoChoiceArray(),
'expanded' => true,
'multiple' => false,
));
$builder->add('responsibleAddress', 'address_type', array(
'required' => true
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Damage',
'cascade_validation' => true,
'validation_groups' =>
function(FormInterface $form) {
$groups = array('damage');
if ($form->getData()->getResponsible() == true) {
$groups[] = 'damage_responsible_address';
}
return $groups;
}
));
}
I'm adding the damage_responsible_address group when responsible is set to true in the form.
Otherwise I don't want the address to be validated.
class ApplianceType extends ...
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('toRepair', 'choice', array(
'choices' => $this->getYesNoChoiceArray(),
'expanded' => true,
'multiple' => false,
));
$builder->add('repairAddress', 'address_type', array(
'required' => true
));
...
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Appliannce',
'cascade_validation' => true,
'validation_groups' =>
function(FormInterface $form) {
$groups = array('appliance');
if ($form->getData()->getToRepair() == true) {
$groups[] = 'appliance_repairer_address';
}
return $groups;
}
));
}
Same as previous, when toRepair is true I want to validate the address.
What's going wrong ?
I the Damage responsible is true and the appliance toRepair is false, the form does give validation errors on the responsible address BUT also on the the appliance address.
The same for the other way arround: When an appliance address is invalid (toRepar is true), then the responsibleAddress is also invalid (even when responsible is false).
The address validation groups don't look on which form they are defined, but just attatch them to every address item in the form.
Is it possible to define validation groups specific for a form only?
I am using Doctrine and Symfony 2.3.6.
The problem is that symfony uses OR-logic for validation groups. So then your apply one of groups to form it will validate address in both cases.
If you move the groups to a parent entity, it will not solve the problem?
/**
* #ORM\Entity()
*/
class Damage extends ...
{
/**
* #ORM\OneToOne(targetEntity="Address", cascade={"persist","remove"})
* #ORM\JoinColumn(name="responsible_address_id", referencedColumnName="id")
* #Assert\Valid(
* groups={
* "damage_responsible_address"
* })
* )
*/
private $responsibleAddress;
}
italic is for general case, bold for the particular case of this question.
The address validation groups don't look on which form they are defined, but just attach them to every address item in the form.
True, your validation_group callback in DamageType set validation_group for all the form, and as you use cascade_validation, for all embed AddressType the form contains. There is no reason it should understand that you want it set only for responsible address.
Generally, setting a validation group through callback for a parent form, with cascade validation set to true, will make all child forms validated against this validation group. If we want to set different validation groups for each children, we have to put a validation group callback in children formtype.
You must place your validation callback in AddressType, so it will be called for each Address Form. Of course, in order to do so, each Adress entity must be aware of its owner.
An alternative to validation callback is Group Sequence Provider. But in any case you still have to set your validation groups in each Address entity and not in Damage, so your Adress must still be aware of its owner.
I know it has been a while, but here is the documentation that may answer the problem with symfony v3:
http://symfony.com/doc/current/form/data_based_validation.html

How to do a form in some steps in Symfony2 - Step Validation

I would like doing a form in some steps in Symfony2 (2.3 exactly), but when I try to do this, I get an error in my form.
I have done the next:
1) I've created a class
class MyClass
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
* #Assert\NotNull()
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="surname", type="string", length=255)
* #Assert\NotNull()
*/
private $surname;
}
2) I've created the FormType class:
class MyClassType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, array('label' => 'name'))
->add('surname', null, array('label' => 'surname'));
}
And I have created 2 more classes to separate the process for getting the data of the form:
class MyClass1Type extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, array('label' => 'name'));
}
class MyClass2Type extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('surname', null, array('label' => 'surname'));
}
And in the controller I have some methods:
public function new1Action()
{
$entity = new MyClass();
$form = $this->createForm( new MyClass1Type( $entity );
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
public function new2Action(Request $request)
{
$entity = new MyClass();
$formMyClass1 = $this->createForm(new MyClass1Type($entity) );
$formMyClass1->bind($request);
if (!$formMyClass1->isValid()) {
print_r($formMyClass1->getErrors());
return new Response("Error");
}
$form = $this->createForm( new MyClass2Type($entity) );
return array(
'entity' => $entity,
'form' => $form->createView(),
);
}
I render the first form (new1Action) and it get the data perfectly, but the problem is when I submit the data. In the new2Action, the application goes throw the response("error") code, because the form is not valid. The print_r() function shows the next information:
Array ( [0] => Symfony\Component\Form\FormError Object ( [message:Symfony\Component\Form\FormError:private] => Este valor no debería ser null. [messageTemplate:protected] => This value should not be null. [messageParameters:protected] => Array ( ) [messagePluralization:protected] => ) )
I think that the problem is that the class is not complete with the data got in the first form, but I need to separate the form in two steps and I have no idea how deal with this error.
Could someone help me?
Thanks in advance.
After binding your entity with MyClass1Type, your entity have a valid name but no surname. $myFormClass1->isValid() returns false, because it try to validate the entity and you didn't specify to validate part of data, so it don't like surname being null.
You should use validation groups to validate your entity on partial data. Check here in Symfony book.
Add in your form :
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => array('validationStep1'),
));
}
And define your validation group on the #Assert annotation on your entity with #Assert\NotNull(groups={"validationStep1"}):
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
* #Assert\NotNull(groups={"validationStep1"})
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="surname", type="string", length=255)
*/
private $surname;