Symfony 5 注册表验证不起作用
Symfony 5 Registration Form validation not working
我是第一次学习 Symfony。
表单验证没有按预期工作,我确实按照 Symfony 文档留下了样板代码,我已经阅读了其他堆栈溢出文章和文档 10 遍以查看是否遗漏了什么,但是我想不通。
客户端验证已关闭,这基本上可以很好地测试服务器端验证。
但是当提交表单时,isValid() 方法 return 即使是空表单和无效数据也是如此。我在 Entity 和 formType 中都添加了约束,但它只是绕过了所有内容,我知道这是事实,因为异常是由数据库或密码编码方法抛出的。
下面有一个简单的代码示例,请检查:
ps:这是一个学习的虚拟项目,
这是代码:
//App\Entity\MyFavorite.php
//App\Controller\RegistrationController:
<?php
namespace App\Controller;
use App\Entity\MyFavorite;
use App\Form\RegistrationFormType;
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\Validator\Validator\ValidatorInterface;
class RegistrationController extends AbstractController
{
/**
* @Route("/register", name="app_register")
*/
public function register(
Request $request,
UserPasswordEncoderInterface $passwordEncoder,
ValidatorInterface $validator): Response
{
$user = new MyFavorite();
$form = $this->createForm(
RegistrationFormType::class, $user
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an email
return $this->redirectToRoute('myfav_vault');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView()
]);
}
}
//App\Entity\MyFavorite
<?php
namespace App\Entity;
use App\Repository\MyFavoriteRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
//-> Bringing in the Validator Constraints as Assert
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=MyFavoriteRepository::class)
* @UniqueEntity(fields={"uniqueCode"},
message="There is already an account with this uniqueCode")
*/
class MyFavorite implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=11, unique=true)
* -> Adding my first custom assertion:
* @Assert\Regex("/^[a-zA-Z0-9]{3}-[a-zA-Z0-9]{3}-[a-zA-Z0-9]. {3}$/")
* @Assert\Length(
* min = 11,
* max = 11
* )
*/
private $uniqueCode;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
/**
* @ORM\Column(type="string", length=100)
* @Assert\NotBlank
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getUniqueCode(): ?string
{
return $this->uniqueCode;
}
public function setUniqueCode(string $uniqueCode): self
{
$this->uniqueCode = $uniqueCode;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->uniqueCode;
}
/**
* @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 insecurity.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user,
// $this->plainPassword = null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getExpiry(): ?\DateTimeInterface
{
return $this->expiry;
}
public function setExpiry(?\DateTimeInterface $expiry): self
{
$this->expiry = $expiry;
return $this;
}
}
//App\Form\RegistrationFormType
<?php
namespace App\Form;
use App\Entity\MyFavorite;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
//-> Getting basic TextType
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
//-> Testing out with Regex and String
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('uniqueCode')
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
//-> I am removing this as I do not want to provide options for min/max length
/*new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),*/
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => MyFavorite::class,
//->Im going to turn of form validation on the client to see if the validation works on the server:
'validation_groups' => false
]);
}
}
//register.html.twig:
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %}
<h1>Register</h1>
{{ form_start(registrationForm, {'attr': {'novalidate':'novalidate'}}) }}
{# This line had to be added manually so the form comes at the right place, some how all fields were not included #}
{{ form_row(registrationForm.name) }}
{{ form_row(registrationForm.uniqueCode) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
<button type="submit" class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}
调查结果:
提交完全空白的表单时:
encodePassword() 抛出异常,因为密码字段为 null 并且它需要字符串,(它甚至不应该进入这个阶段,因为我想象 isValid() 应该 return false?)
使用密码提交,其他所有为空值:
绕过一切,只有数据库抛出异常。
为uniqueCode提交错误的模式:验证器甚至不识别任何东西并且每次都通过它
handleRequest() 方法没有抛出任何异常或 returning false 除了密码我得到一个非常丑陋的错误说密码的参数 2 不能为空,
很明显验证没有发生并且 isValid() 是 returning true.
错误异常来自数据库,
当我尝试插入带有错误模式的唯一代码时,它插入没有问题,
当名称为空时它通过验证但数据库抛出异常,
当密码字段为空时,它仍然通过验证,
Symfony 文档说:
handleRequest() 方法将数据写回同一个对象,
然后我们验证数据:
“在上一节中,您了解了如何使用有效或无效数据提交表单。在 Symfony 中,问题不在于“表单”是否有效,而是底层对象 ($本例中的任务)在表单将提交的数据应用到它之后才有效。调用 $form->isValid() 是询问 $task 对象是否具有有效数据的快捷方式。"
我试图查看 ValidatorInterface 文档,但其中明确指出:
“大多数时候,您不会直接与验证器服务交互或需要担心打印出错误。大多数时候,您将在处理提交的表单数据时间接使用验证。有关更多信息,请参阅如何验证 Symfony 表单。"
我在这里错过了什么?
这并不是 Whosebug 的正确格式,但我们可以看到在关闭之前我们能走多远。可能需要在 Reddit Symfony 论坛上继续。
从小处着手,让以下操作生效。请注意,没有实体或其他东西在进行。只是想说服自己验证的基础知识确实有效:
class RegistrationController extends AbstractController
{
public function register(Request $request)
{
$form = $this->createFormBuilder()
->add('username', TextType::class, [
'constraints' => new NotBlank(['message' => 'User name cannot be blank'])
])
->add('save', SubmitType::class, ['label' => 'Register'])
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dump('Validated');
}
return $this->render('user/register.html.twig',['form' => $form->createView()]);
}
}
# user/register.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<h1>Register Form</h1>
{{ form_start(form, {'attr':{'novalidate':'novalidate'}}) }}
{{ form_end(form) }}
{% endblock %}
我是第一次学习 Symfony。
表单验证没有按预期工作,我确实按照 Symfony 文档留下了样板代码,我已经阅读了其他堆栈溢出文章和文档 10 遍以查看是否遗漏了什么,但是我想不通。
客户端验证已关闭,这基本上可以很好地测试服务器端验证。 但是当提交表单时,isValid() 方法 return 即使是空表单和无效数据也是如此。我在 Entity 和 formType 中都添加了约束,但它只是绕过了所有内容,我知道这是事实,因为异常是由数据库或密码编码方法抛出的。
下面有一个简单的代码示例,请检查:
ps:这是一个学习的虚拟项目, 这是代码:
//App\Entity\MyFavorite.php
//App\Controller\RegistrationController:
<?php
namespace App\Controller;
use App\Entity\MyFavorite;
use App\Form\RegistrationFormType;
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\Validator\Validator\ValidatorInterface;
class RegistrationController extends AbstractController
{
/**
* @Route("/register", name="app_register")
*/
public function register(
Request $request,
UserPasswordEncoderInterface $passwordEncoder,
ValidatorInterface $validator): Response
{
$user = new MyFavorite();
$form = $this->createForm(
RegistrationFormType::class, $user
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an email
return $this->redirectToRoute('myfav_vault');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView()
]);
}
}
//App\Entity\MyFavorite
<?php
namespace App\Entity;
use App\Repository\MyFavoriteRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
//-> Bringing in the Validator Constraints as Assert
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=MyFavoriteRepository::class)
* @UniqueEntity(fields={"uniqueCode"},
message="There is already an account with this uniqueCode")
*/
class MyFavorite implements UserInterface
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\Column(type="string", length=11, unique=true)
* -> Adding my first custom assertion:
* @Assert\Regex("/^[a-zA-Z0-9]{3}-[a-zA-Z0-9]{3}-[a-zA-Z0-9]. {3}$/")
* @Assert\Length(
* min = 11,
* max = 11
* )
*/
private $uniqueCode;
/**
* @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/
private $password;
/**
* @ORM\Column(type="string", length=100)
* @Assert\NotBlank
*/
private $name;
public function getId(): ?int
{
return $this->id;
}
public function getUniqueCode(): ?string
{
return $this->uniqueCode;
}
public function setUniqueCode(string $uniqueCode): self
{
$this->uniqueCode = $uniqueCode;
return $this;
}
/**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{
return (string) $this->uniqueCode;
}
/**
* @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 insecurity.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user,
// $this->plainPassword = null;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getExpiry(): ?\DateTimeInterface
{
return $this->expiry;
}
public function setExpiry(?\DateTimeInterface $expiry): self
{
$this->expiry = $expiry;
return $this;
}
}
//App\Form\RegistrationFormType
<?php
namespace App\Form;
use App\Entity\MyFavorite;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
//-> Getting basic TextType
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Regex;
//-> Testing out with Regex and String
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('uniqueCode')
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
//-> I am removing this as I do not want to provide options for min/max length
/*new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),*/
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => MyFavorite::class,
//->Im going to turn of form validation on the client to see if the validation works on the server:
'validation_groups' => false
]);
}
}
//register.html.twig:
{% extends 'base.html.twig' %}
{% block title %}Register{% endblock %}
{% block body %}
{% for flashError in app.flashes('verify_email_error') %}
<div class="alert alert-danger" role="alert">{{ flashError }}</div>
{% endfor %}
<h1>Register</h1>
{{ form_start(registrationForm, {'attr': {'novalidate':'novalidate'}}) }}
{# This line had to be added manually so the form comes at the right place, some how all fields were not included #}
{{ form_row(registrationForm.name) }}
{{ form_row(registrationForm.uniqueCode) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Password'
}) }}
<button type="submit" class="btn">Register</button>
{{ form_end(registrationForm) }}
{% endblock %}
调查结果:
提交完全空白的表单时: encodePassword() 抛出异常,因为密码字段为 null 并且它需要字符串,(它甚至不应该进入这个阶段,因为我想象 isValid() 应该 return false?)
使用密码提交,其他所有为空值: 绕过一切,只有数据库抛出异常。
为uniqueCode提交错误的模式:验证器甚至不识别任何东西并且每次都通过它
handleRequest() 方法没有抛出任何异常或 returning false 除了密码我得到一个非常丑陋的错误说密码的参数 2 不能为空, 很明显验证没有发生并且 isValid() 是 returning true.
错误异常来自数据库,
当我尝试插入带有错误模式的唯一代码时,它插入没有问题, 当名称为空时它通过验证但数据库抛出异常, 当密码字段为空时,它仍然通过验证,
Symfony 文档说:
handleRequest() 方法将数据写回同一个对象,
然后我们验证数据:
“在上一节中,您了解了如何使用有效或无效数据提交表单。在 Symfony 中,问题不在于“表单”是否有效,而是底层对象 ($本例中的任务)在表单将提交的数据应用到它之后才有效。调用 $form->isValid() 是询问 $task 对象是否具有有效数据的快捷方式。"
我试图查看 ValidatorInterface 文档,但其中明确指出: “大多数时候,您不会直接与验证器服务交互或需要担心打印出错误。大多数时候,您将在处理提交的表单数据时间接使用验证。有关更多信息,请参阅如何验证 Symfony 表单。"
我在这里错过了什么?
这并不是 Whosebug 的正确格式,但我们可以看到在关闭之前我们能走多远。可能需要在 Reddit Symfony 论坛上继续。
从小处着手,让以下操作生效。请注意,没有实体或其他东西在进行。只是想说服自己验证的基础知识确实有效:
class RegistrationController extends AbstractController
{
public function register(Request $request)
{
$form = $this->createFormBuilder()
->add('username', TextType::class, [
'constraints' => new NotBlank(['message' => 'User name cannot be blank'])
])
->add('save', SubmitType::class, ['label' => 'Register'])
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
dump('Validated');
}
return $this->render('user/register.html.twig',['form' => $form->createView()]);
}
}
# user/register.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<h1>Register Form</h1>
{{ form_start(form, {'attr':{'novalidate':'novalidate'}}) }}
{{ form_end(form) }}
{% endblock %}