-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] Introduce validation events for forms #38479
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
src/Symfony/Component/Form/Extension/Validator/EventListener/ValidationListener.php
Outdated
Show resolved
Hide resolved
[Note] A discussion of whether to make these form events be emitted also on children forms was started and not concluded in the previous PR: #37641 (comment) |
Co-authored-by: d-ph <dominikd100@gmail.com>
Hello, Could we get an update about this PR, please? This is a fantastic feature that I believe would be highly welcome when merged. One of those things that aren't immediately obvious that are needed/useful, but that one appreciates when they discover that the feature already exists and is ready to use when they need it. Who is able to accept/feedback the suggested addition of emitting those new form events down the entire form tree? As a compromise, only the post_validate could be emitted recursively, as I personally don't have and never had a use case for doing anything custom just before the validation is run. This would however require a notice in the docs that the pre_validate is being emitted on root forms only, so this course of action has its own downsides. @stof, apologies for at-mentioning you, but would you be able to say how to progress the above, please? Who is able or has the authority to give a 👍 or 👎 to the above? Thanks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could even "auto subscribe" if the data is an object and implements the right interface in FormConfigBuilder
{ | ||
$this->validator = $validator; | ||
$this->violationMapper = $violationMapper; | ||
|
||
if (!$eventDispatcher) { | ||
@trigger_error(sprintf('The "$eventDispatcher" argument to the "%s" constructor will be required in Symfony 6.0.', self::class)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now it is recommended to use the trigger_deprecation
function of the symfony/deprecation-contracts
package
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That tells you how old this is and I'm not really staying on top of this. If someone else would like to take it over, please do!
Since the validation is already a form listener, you can register another listener before and after it. So I fail to see the advantage of this PR. |
The validation happens in a listener and adds its own functionality. I think it's more explicit to do something before or after validation explicitly than when the form is submitted only knowing that validation may have happened or is about to happen. |
This is actually a really good feature. Currently i am facing exactly that issue described in: #17918 (comment) In my case i want to create an image type which automatically handles the upload and the removal of an image on the filesystem and in the database as well. That i only need to use my <?php
declare(strict_types=1);
namespace App\Entity;
use App\Domain\Identifier\ImageId;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
/**
* @ORM\Entity
* @ORM\Table(name="images")
*/
class Image implements \Stringable
{
/**
* @ORM\Id
* @ORM\Column(type="image_id", unique=true)
*/
private ImageId $id;
/**
* @ORM\Column(type="string")
*/
private string $name;
private ?File $file = null;
private bool $delete = false;
public function __construct()
{
$this->id = new ImageId();
}
public function __toString(): string
{
return $this->name;
}
public function getId(): ImageId
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getFile(): ?File
{
return $this->file;
}
public function setFile(?File $file): void
{
$this->file = $file;
}
public function isDelete(): bool
{
return $this->delete;
}
public function setDelete(bool $delete): void
{
$this->delete = $delete;
}
} Then I got my Form: <?php
declare(strict_types=1);
namespace App\Form\Image;
use App\Domain\Enum\Uploader\Context;
use App\Entity\Image;
use App\Uploader\UploaderInterface;
use Doctrine\ORM\EntityManagerInterface;
use League\Flysystem\FilesystemOperator;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Image as ImageConstraint;
use function Safe\sprintf;
final class ImageType extends AbstractType
{
public function __construct(
private UploaderInterface $uploader,
private FilesystemOperator $uploadStorage,
private EntityManagerInterface $entityManager,
) {
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['context'] = (string) $options['context'];
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
/** @var Context $context */
$context = $options['context'];
$builder
->add('name', HiddenType::class)
->add('delete', CheckboxType::class, [
'label' => false,
'required' => false,
'attr' => [
'class' => 'hidden',
],
])
->add('file', FileType::class, [
'constraints' => [
new ImageConstraint([
'allowLandscape' => false,
'allowPortrait' => false,
'maxSize' => '2M',
]),
],
'label' => false,
'required' => false,
'attr' => [
'data-controller' => 'dropzone_controller',
'placeholder' => 'Bild suchen oder hier hinein ziehen',
],
'help' => 'PNG, JPG bis zu 2MB',
])
->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($context): void {
/** @var Image|null $object */
$object = $event->getData();
if (null === $object) {
return;
}
if (!$object->isDelete()) {
return;
}
$event->setData(null);
$this->entityManager->remove($object);
$this->uploadStorage->delete(sprintf('%s/%s', $context, $object->getName()));
})
->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) use ($context): void {
/** @var Image|null $object */
$object = $event->getData();
if (!$event->getForm()->isValid()) {
return;
}
if (null === $object) {
return;
}
if (null === $object->getFile()) {
return;
}
$filename = $this->uploader->upload($object->getFile(), $context);
$object->setName($filename);
$object->setFile(null);
$event->setData($object);
});
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Image::class,
'context' => Context::TEMPORARY(),
]);
}
} And here is the part where i want to use it: <?php
declare(strict_types=1);
namespace App\Form\User;
use App\Domain\Enum\Uploader\Context;
use App\Entity\User;
use App\Form\Image\ImageType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class UpdateAccountFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// more mapped fields
->add('profileImage', ImageType::class, [
'label' => 'Profilbild',
'required' => false,
'context' => Context::PROFILE(),
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
} So in my controller i create the |
You can change the priority of your listener to pass after the validation one |
I did this as well but it was still to early the validation failed but |
@lyrixx, the situation described by @silasjoisten is exactly the reason why I suggested that the |
Shall we close @alcaeus? Time passing means to me this is not super desired. |
Yes, there doesn't seem to be enough interest. It there is, somebody else can recreate this. |
Note: this PR was originally created as #37641 and subsequently closed. This PR contains the same changes, but opened against the 5.x branch. Discussion has not been ported over, so please raise your issues if they are still relevant.
This PR suggests the introduction of two new events related to form handling:
preValidate
andpostValidate
. These events are dispatched by the validation listener before starting validation and after validation has completed. They allow adding additional handling to the form submit process, e.g. writing changes from a DTO to a managed entity after validation succeeds. Consider the following DTO:Now, a controller can create the DTO, pass it to the form, then store the managed entity:
Of course, this process could be further automated to fit one's needs, but I believe that for the time being, the addition of these events makes is a shortcut towards a better handling of DTOs in forms.