diff --git a/src/LiveComponent/src/ComponentWithFormTrait.php b/src/LiveComponent/src/ComponentWithFormTrait.php index fff01e0b73b..c3c1b0f1264 100644 --- a/src/LiveComponent/src/ComponentWithFormTrait.php +++ b/src/LiveComponent/src/ComponentWithFormTrait.php @@ -186,7 +186,7 @@ private function submitForm(bool $validateAll = true): void ); if (!$form->isValid()) { - throw new UnprocessableEntityHttpException('Form validation failed in component.'); + throw new UnprocessableEntityHttpException(\sprintf("Form validation failed:\n%s", implode("\n", $this->extractErrors($form, $form->getName())))); } } @@ -318,4 +318,24 @@ private function clearErrorsForNonValidatedFields(FormInterface $form, string $c $this->clearErrorsForNonValidatedFields($child, \sprintf('%s.%s', $currentPath, $name)); } } + + /** + * @return array + */ + private function extractErrors(FormInterface $form, string $prefix = ''): array + { + $errors = []; + $label = '' !== $prefix ? $prefix : 'form'; + + foreach ($form->getErrors() as $error) { + $errors[] = $label.': '.$error->getMessage(); + } + + foreach ($form->all() as $name => $child) { + $childPath = '' !== $prefix ? \sprintf('%s.%s', $prefix, $name) : (string) $name; + array_push($errors, ...$this->extractErrors($child, $childPath)); + } + + return $errors; + } } diff --git a/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php b/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php index 1686b9a9987..aa38854107a 100644 --- a/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php +++ b/src/LiveComponent/tests/Fixtures/Component/FormWithCollectionTypeComponent.php @@ -53,4 +53,10 @@ public function removeComment(#[LiveArg] int $index) { unset($this->formValues['comments'][$index]); } + + #[LiveAction] + public function save(): void + { + $this->submitForm(); + } } diff --git a/src/LiveComponent/tests/Fixtures/Form/CommentFormType.php b/src/LiveComponent/tests/Fixtures/Form/CommentFormType.php index 37ebd4e8a3f..3087b073fa0 100644 --- a/src/LiveComponent/tests/Fixtures/Form/CommentFormType.php +++ b/src/LiveComponent/tests/Fixtures/Form/CommentFormType.php @@ -17,12 +17,18 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Comment; +use Symfony\Component\Validator\Constraints\NotBlank; + class CommentFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('content', TextareaType::class) + ->add('content', TextareaType::class, [ + 'constraints' => [ + new NotBlank(message: 'The comment content field should not be blank'), + ], + ]) ; } diff --git a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php index cda66c1fcb7..58417e4b5d9 100644 --- a/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php +++ b/src/LiveComponent/tests/Functional/Form/ComponentWithFormTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Symfony\UX\LiveComponent\Tests\Fixtures\Component\FormWithCollectionTypeComponent; use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\User; use Symfony\UX\LiveComponent\Tests\Fixtures\Factory\CategoryFixtureEntityFactory; @@ -488,4 +489,40 @@ public function testFormWithLivePropContainingAnEntityImplementingAnInterface() self::assertEquals(1, $user->id); self::assertEquals('Nicolas', $user->username); } + + public function testSubmitFormExceptionMessageContainsFieldPathsAndMessages() + { + $mounted = $this->mountComponent('form_with_collection_type'); + $dehydratedProps = $this->dehydrateComponent($mounted)->getProps(); + + $exception = null; + + try { + $this->browser() + ->throwExceptions() + ->post('/_components/form_with_collection_type/save', [ + 'body' => [ + 'data' => json_encode([ + 'props' => $dehydratedProps, + 'updated' => [ + 'blog_post_form.title' => '', + 'blog_post_form.content' => 'too short', + 'blog_post_form.comments' => [['content' => '']], + ], + ]), + ], + ]) + ; + } catch (UnprocessableEntityHttpException $e) { + $exception = $e; + } + + $this->assertNotNull($exception); + $this->assertStringContainsString('title', $exception->getMessage()); + $this->assertStringContainsString('content', $exception->getMessage()); + $this->assertStringContainsString('The title field should not be blank', $exception->getMessage()); + $this->assertStringContainsString('The content field is too short', $exception->getMessage()); + $this->assertStringContainsString('blog_post_form.comments.0.content', $exception->getMessage()); + $this->assertStringContainsString('The comment content field should not be blank', $exception->getMessage()); + } }