如何实现一个简单的注册表单

3.4 版本
维护中的版本

创建一个注册表单是非常容易的 - 它事实上意味着,你只需要创建一个表单,表单将更新一些User的模型对象(这个例子是一个Doctrine实体)并保存它。

受欢迎的FOSUserBundle 提供了一个注册表单,重置密码表单和其他用户管理功能。

如果你先前没有一个User实体和能工作的登录系统,你要先从怎样从数据库加载安全用户开始。

你的User实体至少应该有以下字段:

username

他是用来登录的,除非你想用email来替代你的用户(在那种情况下,这个字段就不是必要的了)。

email

这是一条不错的信息,很值得收集。您也可以允许用户通过email登录。

password

编译的密码

plainPassword

这个字段不会被持久化:(注意没有上面的@ORM\Column)。他将临时存储注册表单的明文密码。此字段可以被验证,然后被用于password字段的填充。

添加了一些验证,你的类可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// src/AppBundle/Entity/User.php
namespace AppBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
 
/**
 * @ORM\Entity
 * @UniqueEntity(fields="email", message="Email already taken")
 * @UniqueEntity(fields="username", message="Username already taken")
 */
class User implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;
 
    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\NotBlank()
     * @Assert\Email()
     */
    private $email;
 
    /**
     * @ORM\Column(type="string", length=255, unique=true)
     * @Assert\NotBlank()
     */
    private $username;
 
    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=4096)
     */
    private $plainPassword;
 
    /**
     * The below length depends on the "algorithm" you use for encoding
     * the password, but this works well with bcrypt.
     *
     * @ORM\Column(type="string", length=64)
     */
    private $password;
 
    // other properties and methods
 
    public function getEmail()
    {
        return $this->email;
    }
 
    public function setEmail($email)
    {
        $this->email = $email;
    }
 
    public function getUsername()
    {
        return $this->username;
    }
 
    public function setUsername($username)
    {
        $this->username = $username;
    }
 
    public function getPlainPassword()
    {
        return $this->plainPassword;
    }
 
    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }
 
    public function setPassword($password)
    {
        $this->password = $password;
    }
 
    public function getSalt()
    {
        // The bcrypt algorithm doesn't require a separate salt.
        // You *may* need a real salt if you choose a different encoder.
        return null;
    }
 
    // other methods, including security methods like getRoles()
}

UserInterface要求要有一些其他的方法,并且你的security.yml文件需要被正确配置,来让User实体工作。更多完整的例子,参见实体提供器文章。

为什么限制4096密码 

注意,plainPassword字段的最大长度是4096字符。为了安全起见(CVE-2013-5750),当编译它时,Symfony限制明文密码长度到4096字符。添加此约束来确保如果有人尝试了一个超长的密码,你的表单应该提示一个验证错误。

你需要添加这个约束到你应用程序任何需要用户提交明文密码的地方(如,修改密码表单)。唯一不需要你担心的就是你的登录表单,因为symfony安全组件会替你处理。

为实体创建一个表单 

下一步,给User实体创建表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/AppBundle/Form/UserType.php
namespace AppBundle\Form;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
 
class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email', EmailType::class)
            ->add('username', TextType::class)
            ->add('plainPassword', RepeatedType::class, array(
                'type' => PasswordType::class,
                'first_options'  => array('label' => 'Password'),
                'second_options' => array('label' => 'Repeat Password'),
            )
        );
    }
 
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\User',
        ));
    }
}

这里有两个字段:email, usernameplainPassword(重复确认输入的密码)。

探索更多关于表单组件的事情,请阅读表单指南。

处理表单提交 

下一步,你需要一个控制器去处理表单渲染和提交。如果表单被提交,控制器执行验证并保存数据到数据库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/AppBundle/Controller/RegistrationController.php
namespace AppBundle\Controller;
 
use AppBundle\Form\UserType;
use AppBundle\Entity\User;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
 
class RegistrationController extends Controller
{
    /**
     * @Route("/register", name="user_registration")
     */
    public function registerAction(Request $request)
    {
        // 1) build the form
        $user = new User();
        $form = $this->createForm(UserType::class, $user);
 
        // 2) handle the submit (will only happen on POST)
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
 
            // 3) Encode the password (you could also do this via Doctrine listener)
            $password = $this->get('security.password_encoder')
                ->encodePassword($user, $user->getPlainPassword());
            $user->setPassword($password);
 
            // 4) save the User!
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();
 
            // ... do any other work - like sending them an email, etc
            // maybe set a "flash" success message for the user
 
            return $this->redirectToRoute('replace_with_some_route');
        }
 
        return $this->render(
            'registration/register.html.twig',
            array('form' => $form->createView())
        );
    }
}

在安全配置中配置上面步骤3的编码器,来定义用于编译密码的算法:

1
2
3
4
# app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User: bcrypt
1
2
3
4
5
6
7
8
9
10
11
<!-- app/config/security.xml -->
<?xml version="1.0" charset="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <config>
        <encoder class="AppBundle\Entity\User">bcrypt</encoder>
    </config>
</srv:container>
1
2
3
4
5
6
// app/config/security.php
$container->loadFromExtension('security', array(
    'encoders' => array(
        'AppBundle\Entity\User' => 'bcrypt',
    ),
));

这个案例我们推荐使用bcrypt 算法。了解更多关于如何编码用户密码的细节请看安全章节

如果您决定不使用注释方式的路由(如上),那么你需要创建一个这个控制器的路由:

1
2
3
4
# app/config/routing.yml
user_registration:
    path:     /register
    defaults: { _controller: AppBundle:Registration:register }
1
2
3
4
5
6
7
8
9
10
<!-- app/config/routing.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
 
    <route id="user_registration" path="/register">
        <default key="_controller">AppBundle:Registration:register</default>
    </route>
</routes>
1
2
3
4
5
6
7
8
9
10
// app/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
 
$collection = new RouteCollection();
$collection->add('user_registration', new Route('/register', array(
    '_controller' => 'AppBundle:Registration:register',
)));
 
return $collection;

下一步,创建模板:

1
2
3
4
5
6
7
8
9
10
{# app/Resources/views/registration/register.html.twig #}
 
{{ form_start(form) }}
    {{ form_row(form.username) }}
    {{ form_row(form.email) }}
    {{ form_row(form.plainPassword.first) }}
    {{ form_row(form.plainPassword.second) }}
 
    <button type="submit">Register!</button>
{{ form_end(form) }}
1
2
3
4
5
6
7
8
9
10
11
<!-- app/Resources/views/registration/register.html.php -->
 
<?php echo $view['form']->start($form) ?>
    <?php echo $view['form']->row($form['username']) ?>
    <?php echo $view['form']->row($form['email']) ?>
 
    <?php echo $view['form']->row($form['plainPassword']['first']) ?>
    <?php echo $view['form']->row($form['plainPassword']['second']) ?>
 
    <button type="submit">Register!</button>
<?php echo $view['form']->end($form) ?>

参见如何自定义表单渲染,里面有更多细节。

更新你的数据库结构 

如果你在这个教程中已经更新了User实体,你必须要使用下面的命令去更新数据库结构:

1
$ php bin/console doctrine:schema:update --force

就是这样!来到/register来尝试一下吧!

注册表单只有Email(没有 Username) 

如果你想要你的用户通过email登录并不需要用户名,那么你可以从你的User实体中彻底移除他。相反,让getUsername()返回email属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/AppBundle/Entity/User.php
// ...
 
class User implements UserInterface
{
    // ...
 
    public function getUsername()
    {
        return $this->email;
    }
 
    // ...
}

下一步,只更改你security.yml文件的providers 部分,以便Symfony知道如何去通过email属性加载你的用户来登录。参见如何自定义表单渲染

添加一个“打上勾”的Checkbox 

有时,你想要一个“你接受这个条款和声明吗?”的Checkbox,出现在你的注册表单。唯一窍门,让你要去添加这个字段到你的表单中,而你永远不需要添加多余的termsAccepted属性到你的User实体。

要做到这一点,要添加一个termsAccepted字段到你的表单,但设置它的 mapped 选项为false:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Form/UserType.php
// ...
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
 
class UserType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('email', EmailType::class);
            // ...
            ->add('termsAccepted', CheckboxType::class, array(
                'mapped' => false,
                'constraints' => new IsTrue(),
            ))
        );
    }
}

constraints配置也被使用了,它允许我们添加验证,尽管没有User中没有termsAccepted属性。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)