0

How do I see validation errors when posting json data and self submitting to a symfony form? isValid() is false but I can't access error messages to return. But the Symfony Profiler DOES show the error messages in the Ajax request history. E.g. when duplicate username the profiler shows:

Validator calls in ValidationListener.php data.username There is already an account with this username

Forms "registration_form" "App\Form\RegistrationFormType" There is already an account with this username Caused by: Symfony\Component\Validator\ConstraintViolation

When all the fields are valid the new User is created in the database successfully as expected.

Here is my controller:

namespace App\Controller;

use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\LoginFormAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;

class RegistrationController extends AbstractController
{
    /**
     * @Route("/api/register", name="app_register")
     */
    public function register(
        Request $request,
        UserPasswordEncoderInterface $passwordEncoder,
        GuardAuthenticatorHandler $guardHandler,
        LoginFormAuthenticator $authenticator
    ): Response {
        if ($request->isMethod('POST')) {
            $user = new User();
            $form = $this->createForm(RegistrationFormType::class, $user);

            $data = json_decode($request->getContent(), true);

            $form->submit($data);

            if ($form->isSubmitted()) {
                if ($form->isValid()) {
                    $user->setPassword(
                        $passwordEncoder->encodePassword(
                            $user,
                            $form->get('plainPassword')->getData()
                        )
                    );

                    $em = $this->getDoctrine()->getManager();
                    $em->persist($user);
                    $em->flush();

                    // login the newly registered user
                    $login = $guardHandler->authenticateUserAndHandleSuccess(
                        $user,
                        $request,
                        $authenticator,
                        'main' // firewall name in security.yaml
                    );

                    if ($login !== null) {
                        return $login;
                    }

                    return $this->json([
                        'username' => $user->getUsername(),
                        'roles' => $user->getRoles(),
                    ]);
                } else {
                    $formErrors = $form->getErrors(true); // returns {}
                    return $this->json($formErrors, Response::HTTP_BAD_REQUEST);
                }
            }
        }
    }

Here is my RegistrationFormType:

namespace App\Form;

use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;

class RegistrationFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstName', TextType::class, [
                'label' => 'First Name',
                'required' => false
            ])
            ->add('lastName', TextType::class, [
                'label' => 'Last Name',
                'required' => false
            ])
            ->add('username')
            ->add('emailAddress', EmailType::class, [
                'label' => 'Email Address'
            ])
            ->add('plainPassword', PasswordType::class, [
                'mapped' => false
            ])
            ->add('agreeTerms', CheckboxType::class, [
                'mapped' => false,
                'constraints' => [
                    new IsTrue([
                        'message' => 'You must comply.',
                    ]),
                ],
            ])
            ->add('Register', SubmitType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
            'csrf_protection' => false
        ]);
    }

    public function getName()
    {
        return 'registration_form';
    }
}

Here is my entity:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @UniqueEntity(fields={"username"}, message="There is already an account with this username")
 * @UniqueEntity(fields={"emailAddress"}, message="There is already an account with this email address")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $username;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    /**
     * @var string The hashed password
     * @ORM\Column(type="string")
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=180, unique=true)
     */
    private $emailAddress;

    /**
     * @ORM\Column(type="string", length=80, nullable=true)
     */
    private $firstName;

    /**
     * @ORM\Column(type="string", length=80, nullable=true)
     */
    private $lastName;

    public function getId(): ?int
    {
        return $this->id;
    }

    /**
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUsername(): string
    {
        return (string) $this->username;
    }

    public function setUsername(string $username): self
    {
        $this->username = $username;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function getPassword(): string
    {
        return (string) $this->password;
    }

    public function setPassword(string $password): self
    {
        $this->password = $password;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function getSalt()
    {
        // not needed when using the "bcrypt" algorithm in security.yaml
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }

    public function getEmailAddress(): ?string
    {
        return $this->emailAddress;
    }

    public function setEmailAddress(string $emailAddress): self
    {
        $this->emailAddress = $emailAddress;

        return $this;
    }

    public function getFirstName(): ?string
    {
        return $this->firstName;
    }

    public function setFirstName(?string $firstName): self
    {
        $this->firstName = $firstName;

        return $this;
    }

    public function getLastName(): ?string
    {
        return $this->lastName;
    }

    public function setLastName(?string $lastName): self
    {
        $this->lastName = $lastName;

        return $this;
    }
}
1
  • Try to use snake_case in form type for field names instead of camelCase. For example: firstName => first_name . I had a similar problem and this solved it Commented Dec 22, 2020 at 7:19

2 Answers 2

0

Got it working.

$formErrors = [];
foreach ($form->all() as $childForm) {
    if ($childErrors = $childForm->getErrors()) {
        foreach ($childErrors as $error) {
            $formErrors[$error->getOrigin()->getName()] = $error->getMessage();
        }
    }
}

return $this->json(
    ['errors' => $formErrors],
    Response::HTTP_BAD_REQUEST
);

Returns:

{
    "errors": {
        "username": "There is already an account with this username"
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

Basically you get form errors at the top level like if there are extra fields etc but you get the form field errors from the actual child elements. You are only returning the top level form errors.

I have a helper function I use to return all errors from a form if I am returning a JsonResponse.

The following is my extended abstract controller. All my controllers extend this and then I can keep a few helper methods here.

I use the createNamedForm() method and keep the name blank as it's easier when sending from Ajax as I don't need to nest the data in the array with the form name as the key.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as SymfonyAbstractController;
use Symfony\Component\Form\FormInterface;

class AbstractController extends SymfonyAbstractController
{
    protected function createNamedForm(string $name, string $type, $data = null, array $options = []): FormInterface
    {
        return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
    }

    protected function getFormErrors($form)
    {
        $errors = [];

        foreach ($form->getErrors() as $error) {
            $errors[] = $error->getMessage();
        }

        foreach ($form->all() as $childForm) {
            if ($childForm instanceof FormInterface) {
                if ($childErrors = $this->getFormErrors($childForm)) {
                    $errors[$childForm->getName()] = $childErrors;
                }
            }
        }

        return $errors;
    }
}

I would create the form in the following way:

$form = $this->createNamedForm('', RegistrationFormType::class);

and return my form errors as follows:

return $this->json($this->getFormErrors($form), 400)

If you want to use the normal createForm() function then you will need to send the data in the following format:

['registration_form' => ['firstName' => 'Tom', 'lastName' => 'Thumb']]

This works in both Symfony4 and Symfony5

5 Comments

It still only results in an empty array.
Sorry I missed the function call as it uses recursion. Yours won't catch top level errors whereas my code will catch all possible form errors.
When I tried your code, $childForm is not an instance of FormInterface. Also I don't think getFormErrors() exists anymore. I'm using Symfony 5.0.
getFormErrors() is my helper function. I normally create a new AbstractController which extends the Symfony one and I keep a few helper functions that I use in my controllers that being one of them. That makes sense why it doesn't work, it works for Symfony 4. On Symfony 5 the form errors class has changed slightly, Ill create a new Symfony5 project tonight and update the code accordingly.
Sorry for the delay I have checked it on a Symfony 5 project and it all works the same as Symfony 4. I have expanded the answer as the difference is how you are creating the form. You have two options and I given you both solutions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.