Describe the bug
I have a custom action , with a custom path, that display a custom form to import a CSV, as the errors are not related to one specific field of the form, I use the $form->addError(new FormError()) to attach the error to the form
I then display it using simply in twig {{ form(form) }} and then the error get displayed twice

Though the form has correctly only one error

To Reproduce
version 3.1.9
(OPTIONAL) Additional context
If they are useful, include logs, code samples, screenshots, etc.
it seems to be due in form_theme.html.twig that these lines :
{% block form_start %}
{% if form.vars.errors|length > 0 %}
{{ form_errors(form) }}
{% endif %}
{{ parent() }}
<input type="hidden" name="referrer" value="{{ ea.request.query.get('referrer') }}">
{% endblock form_start %}
is superflous (the if errors) because it's already handle in form_widget(form)
@allan-simon thanks for reporting this. In #4025 I'm trying to fix this ... but I don't understand Symfony Forms well, so I don't know if this will fix or break things 馃槥 Please, help me reviewing it. Thanks!
@javiereguiluz it's look good to me in term of following what is usually seen in symfony form's theme (as form_start does not have the responsability of displaying errors )
Hello @javiereguiluz ,
Since this modification, errors from CustomValidator doesn't appear on my form.
I feared that "fixing" this would "break" other things 馃槶 I'm really sorry but I don't understand Symfony Forms well enough. Someone who does will need to contribute a fix for this. Thanks.
@vtiertant Can you create a small example application that allows to reproduce it?
@xabbuh :
In our entity, you can call a custom validator with anotations :
<?php
namespace App\Entity;
use App\Repository\CustomEntityRepository;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=ImportOrderMappingRepository::class)
* @Assert\Callback({"App\Validator\CustomValidator", "validate"})
*/
class CustomEntity
{
...
}
And the custom validator :
<?php
namespace App\Validator;
use App\Entity\CustomEntity;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class CustomValidator
{
public static function validate(CustomEntity $customEntity, ExecutionContextInterface $context, $payload)
{
//Do condition with your entity attributes
$message = 'Test error validation';
$context->buildViolation($message)
->addViolation()
;
}
}
This code will display an error when you submit the form, but with this modification, errors does not appear.
(In despiste of error is present in profiler)
Can you fork the Symfony demo or create a small example yourself that allows to reproduce this?
@xabbuh ok a small example :
AppEntityPost
<?php
namespace App\Entity;
use App\Repository\PostRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=PostRepository::class)
* @Assert\Callback({"App\Validator\PostValidator", "validate"})
*/
class Post
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $title;
/**
* @ORM\Column(type="text", nullable=true)
*/
private $content;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $author;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): self
{
$this->title = $title;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): self
{
$this->content = $content;
return $this;
}
public function getAuthor(): ?string
{
return $this->author;
}
public function setAuthor(?string $author): self
{
$this->author = $author;
return $this;
}
}
AppRepositoryPostRepository
<?php
namespace App\Repository;
use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Post|null find($id, $lockMode = null, $lockVersion = null)
* @method Post|null findOneBy(array $criteria, array $orderBy = null)
* @method Post[] findAll()
* @method Post[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
// /**
// * @return Post[] Returns an array of Post objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('p')
->andWhere('p.exampleField = :val')
->setParameter('val', $value)
->orderBy('p.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Post
{
return $this->createQueryBuilder('p')
->andWhere('p.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
AppControllerPostCrudController
<?php
namespace App\Controller;
use App\Entity\Post;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextareaField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
class PostCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Post::class;
}
public function configureFields(string $pageName): iterable
{
return [
TextField::new('title'),
TextareaField::new('content'),
TextField::new('author'),
];
}
/*
public function configureFields(string $pageName): iterable
{
return [
IdField::new('id'),
TextField::new('title'),
TextEditorField::new('description'),
];
}
*/
}
AppValidatorPostValidator
<?php
namespace App\Validator;
use App\Entity\Post;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class PostValidator
{
public static function validate(Post $post, ExecutionContextInterface $context, $payload)
{
if ($post->getContent() != 'Easy admin is great !') {
$context->buildViolation('Test error validation')
->addViolation()
;
}
}
}
in your DashboardController, add your Post Crud Controller Link :
yield MenuItem::linkToCrud(
'post',
null,
PostCrudController::getEntityFqcn()
);
Try to create a new Post :

The redirect is the same page, but the error is not display.
However error is present in profiler :

When I give the code before the correction in easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig :
{% block form_start %}
{% if form.vars.errors|length > 0 %}
{{ form_errors(form) }}
{% endif %}
{{ parent() }}
<input type="hidden" name="referrer" value="{{ ea.request.query.get('referrer') }}">
{% endblock form_start %}
The error now appears when I validate the form :

I haven't looked at the code , but to my knowledge of symfony forms which @xabbuh may confirms, i think it's because the crud forms from easyadminbundle were not calling form_errors(form) (which display the fom's errors which are not mapped to a specific field, like your custom validator which is mapped to the whole entity, and instead the bundle was relying on the fact that their overriden form_start() was calling it implicitly.
However that behaviour this calling tree is different from the default form theme provided by symfony
once again , to be confirmed by someone more expert than me in css but after digging a bit more
in symfony's form
you have
- form_start
- form_widget
- form_end
with form_widget taking care of the displaying of errors as shown here
https://github.com/symfony/symfony/blob/5.x/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig#L23
(hence why i'm pretty sure it was not correct to have the display of errors made by form_start in the previous release of this bundle)
so somehow , as form_widget_compound is redefined here https://github.com/EasyCorp/EasyAdminBundle/blob/master/src/Resources/views/crud/form_theme.html.twig#L185 it may not enter /call the if to display global errors , causing global validation error to not be displayed at all like what @vtiertant is observing
If i call addPath function when i create my custom constraint, i can specify a property path. Exemple :
$context->buildViolation('Test error validation')
->atPath('content')
->addViolation()
;
So, the notification appear under "content" field, even if i delete the following code in form_start :
{% if form.vars.errors|length > 0 %}
{{ form_errors(form) }}
{% endif %}
But the notification is not call twice with this code.
Even if i not remove the code in form_start, error is never call twice in my application everywhere i use a constraint form.
Exemples :
But my problem is : atPath does'nt work for a collection field
But for me, the problem is only with $form->addError(...) but i don't know this function, i never use it and it's not in official symfony documentation
If you try with an annotation or custom validator, have you the same problem ?
The problem is that this addError is linked to a field that is not mapped to an entity (as it's a form to upload a CSV file to bulk import entities), and I use add error to precise if the csv if there's an issue during the bulk import, so going your way would require too much boiler plate for a one time action.
I understand your frustration and I do agree that you're usage is correct and should not be broken by whichever change is made, but as I'm using the public API of the form component, I also expect mine to work :) especially as I think it boils down to a bug/miusage in the form_theme of the bundle.
If one of you is able to provide me an application that I could just clone and that comes with all the use cases you think should work, I will happily look into how to make that possible.
@xabbuh in some hours I will make a small project on github with both use cases , thanks for your time.
https://github.com/allan-simon/form_error_reproduction for the moment there's only the use case of @vtiertant , i'm adding mine in the following hour
@xabbuh done, the code contains both , my code will be called through the import depuis csv action , and @vtiertant 's code through the 'new customer' action , there's no login etc. needed (but it does require to spin up a database and run the doctrine migration )
Thanks, this helped a lot. 馃憤 After looking into it I found the root cause here: For all the form types provided by the CRUD logic of this bundle the CrudFormType is used which defines ea_crud as the block prefix. This means that its widgets are rendered by the ea_crud_widget which currently does not render form errors. For all other form types this block prefix is not set which means that the core Symfony widgets will be used which (since #4025) correctly render the errors.
see #4068
thanks for the PR and the investigation :)
Thx @xabbuh !
Most helpful comment
@xabbuh in some hours I will make a small project on github with both use cases , thanks for your time.