diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst index 4e4abbb5a05..58956777b04 100644 --- a/cookbook/doctrine/registration_form.rst +++ b/cookbook/doctrine/registration_form.rst @@ -1,8 +1,9 @@ .. index:: single: Doctrine; Simple Registration Form single: Form; Simple Registration Form + single: Security; Simple Registration Form -How to Implement a simple Registration Form +How to Implement a Simple Registration Form =========================================== Some forms have extra fields whose values don't need to be stored in the @@ -10,13 +11,15 @@ database. For example, you may want to create a registration form with some extra fields (like a "terms accepted" checkbox field) and embed the form that actually stores the account information. -The simple User Model ---------------------- +.. _the-simple-user-model: + +The Simple User Entity +---------------------- You have a simple ``User`` entity mapped to the database:: - // src/Acme/AccountBundle/Entity/User.php - namespace Acme\AccountBundle\Entity; + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -36,7 +39,7 @@ You have a simple ``User`` entity mapped to the database:: protected $id; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="string", length=255, unique=true) * @Assert\NotBlank() * @Assert\Email() */ @@ -45,9 +48,9 @@ You have a simple ``User`` entity mapped to the database:: /** * @ORM\Column(type="string", length=255) * @Assert\NotBlank() - * @Assert\Length(max = 4096) + * @Assert\Length(max=4096) */ - protected $plainPassword; + protected $password; public function getId() { @@ -64,21 +67,27 @@ You have a simple ``User`` entity mapped to the database:: $this->email = $email; } - public function getPlainPassword() + public function getPassword() { - return $this->plainPassword; + return $this->password; } - public function setPlainPassword($password) + public function setPassword($password) { - $this->plainPassword = $password; + $this->password = $password; + } + + public function getSalt() + { + return null; } } This ``User`` entity contains three fields and two of them (``email`` and -``plainPassword``) should display on the form. The email property must be unique -in the database, this is enforced by adding this validation at the top of -the class. +``password``) should be displayed by the form. The ``email`` property must +be unique in the database, this is enforced by adding an ``@UniqueEntity`` +validation constraint at the top of the class for application-side validation +and by adding ``unique=true`` to the column mapping for the database schema. .. note:: @@ -90,7 +99,7 @@ the class. .. sidebar:: Why the 4096 Password Limit? - Notice that the ``plainPassword`` field has a max length of 4096 characters. + Notice that the ``password`` field has a max length of 4096 characters. For security purposes (`CVE-2013-5750`_), Symfony limits the plain password length to 4096 characters when encoding it. Adding this constraint makes sure that your form will give a validation error if anyone tries a super-long @@ -101,13 +110,15 @@ the class. only place where you don't need to worry about this is your login form, since Symfony's Security component handles this for you. -Create a Form for the Model ---------------------------- +.. _create-a-form-for-the-model: -Next, create the form for the ``User`` model:: +Create a Form for the Entity +---------------------------- - // src/Acme/AccountBundle/Form/Type/UserType.php - namespace Acme\AccountBundle\Form\Type; +Next, create the form for the ``User`` entity:: + + // src/AppBundle/Form/Type/UserType.php + namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -118,17 +129,17 @@ Next, create the form for the ``User`` model:: public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('email', 'email'); - $builder->add('plainPassword', 'repeated', array( - 'first_name' => 'password', - 'second_name' => 'confirm', - 'type' => 'password', + $builder->add('password', 'repeated', array( + 'first_name' => 'password', + 'second_name' => 'confirm', + 'type' => 'password', )); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( - 'data_class' => 'Acme\AccountBundle\Entity\User' + 'data_class' => 'AppBundle\Entity\User' )); } @@ -138,7 +149,7 @@ Next, create the form for the ``User`` model:: } } -There are just two fields: ``email`` and ``plainPassword`` (repeated to confirm +There are just two fields: ``email`` and ``password`` (repeated to confirm the entered password). The ``data_class`` option tells the form the name of the underlying data class (i.e. your ``User`` entity). @@ -156,17 +167,17 @@ be stored in the database. Start by creating a simple class which represents the "registration":: - // src/Acme/AccountBundle/Form/Model/Registration.php - namespace Acme\AccountBundle\Form\Model; + // src/AppBundle/Form/Model/Registration.php + namespace AppBundle\Form\Model; use Symfony\Component\Validator\Constraints as Assert; - use Acme\AccountBundle\Entity\User; + use AppBundle\Entity\User; class Registration { /** - * @Assert\Type(type="Acme\AccountBundle\Entity\User") + * @Assert\Type(type="AppBundle\Entity\User") * @Assert\Valid() */ protected $user; @@ -200,8 +211,8 @@ Start by creating a simple class which represents the "registration":: Next, create the form for this ``Registration`` model:: - // src/Acme/AccountBundle/Form/Type/RegistrationType.php - namespace Acme\AccountBundle\Form\Type; + // src/AppBundle/Form/Type/RegistrationType.php + namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -211,11 +222,9 @@ Next, create the form for this ``Registration`` model:: public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('user', new UserType()); - $builder->add( - 'terms', - 'checkbox', - array('property_path' => 'termsAccepted') - ); + $builder->add('termsAccepted', 'checkbox', array( + 'label' => 'Terms accepted', + )); $builder->add('Register', 'submit'); } @@ -233,121 +242,118 @@ of the ``User`` class. Handling the Form Submission ---------------------------- -Next, you need a controller to handle the form. Start by creating a simple -controller for displaying the registration form:: +Next, you need a controller to handle the form rendering and submission. If the +form is submitted, the controller performs the validation and saves the data +into the database:: - // src/Acme/AccountBundle/Controller/AccountController.php - namespace Acme\AccountBundle\Controller; + // src/AppBundle/Controller/AccountController.php + namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; + use Symfony\Component\HttpFoundation\Request; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - use Acme\AccountBundle\Form\Type\RegistrationType; - use Acme\AccountBundle\Form\Model\Registration; + use AppBundle\Form\Type\RegistrationType; + use AppBundle\Form\Model\Registration; class AccountController extends Controller { - public function registerAction() + /** + * @Route("/register", name="account_register") + */ + public function registerAction(Request $request) { - $registration = new Registration(); - $form = $this->createForm(new RegistrationType(), $registration, array( - 'action' => $this->generateUrl('account_create'), - )); - - return $this->render( - 'AcmeAccountBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); - } - } + $form = $this->createForm(new RegistrationType(), new Registration()); -And its template: + $form->handleRequest($request); -.. code-block:: html+jinja + if ($form->isSubmitted() && $form->isValid()) { + $registration = $form->getData(); + $user = $registration->getUser(); - {# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #} - {{ form(form) }} + $password = $this + ->get('security.encoder_factory') + ->getEncoder($user) + ->encodePassword( + $user->getPassword(), + $user->getSalt() + ); + $user->setPassword($password); -Next, create the controller which handles the form submission. This performs -the validation and saves the data into the database:: + $em = $this->getDoctrine()->getManager(); + $em->persist($user); + $em->flush(); - use Symfony\Component\HttpFoundation\Request; - // ... + return $this->redirect($this->generateUrl('homepage')); + } - public function createAction(Request $request) - { - $em = $this->getDoctrine()->getManager(); - - $form = $this->createForm(new RegistrationType(), new Registration()); - - $form->handleRequest($request); - - if ($form->isValid()) { - $registration = $form->getData(); - - $em->persist($registration->getUser()); - $em->flush(); - - return $this->redirect(...); + return $this->render( + 'account/register.html.twig', + array('form' => $form->createView()) + ); } - - return $this->render( - 'AcmeAccountBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); } -Add new Routes --------------- - -Next, update your routes. If you're placing your routes inside your bundle -(as shown here), don't forget to make sure that the routing file is being -:ref:`imported `. +Storing plain-text passwords is bad practice, so before saving the user +data into the database the submitted plain-text password is replaced by +an encoded one. To define the algorithm used to encode the password +configure the encoder in the security configuration: .. configuration-block:: .. code-block:: yaml - # src/Acme/AccountBundle/Resources/config/routing.yml - account_register: - path: /register - defaults: { _controller: AcmeAccountBundle:Account:register } - - account_create: - path: /register/create - defaults: { _controller: AcmeAccountBundle:Account:create } + # app/config/security.yml + security: + encoders: + AppBundle\Entity\User: bcrypt .. code-block:: xml - - - + + + 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"> - - AcmeAccountBundle:Account:register - - - - AcmeAccountBundle:Account:create - - + + bcrypt + + .. code-block:: php - // src/Acme/AccountBundle/Resources/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; + // app/config/security.php + $container->loadFromExtension('security', array( + 'encoders' => array( + 'AppBundle\Entity\User' => 'bcrypt', + ), + )); + +In this case the recommended ``bcrypt`` algorithm is used. To learn more +about how to encode the users password have a look into the +:ref:`security chapter ` + +.. note:: + + While ``$form->isSubmitted()`` isn't technically needed, since ``isValid()`` + first calls ``isSubmitted()``, it is recommended to use it to improve + readability. + +And its template: + +.. code-block:: html+jinja - $collection = new RouteCollection(); - $collection->add('account_register', new Route('/register', array( - '_controller' => 'AcmeAccountBundle:Account:register', - ))); - $collection->add('account_create', new Route('/register/create', array( - '_controller' => 'AcmeAccountBundle:Account:create', - ))); + {# app/Resources/views/account/register.html.twig #} + {{ form(form) }} + +Add New Routes +-------------- - return $collection; +Don't forget to make sure that the routes defined as annotations in your +controller are :ref:`loaded ` by your main +routing configuration file. Update your Database Schema --------------------------- @@ -360,8 +366,8 @@ sure that your database schema has been updated properly: $ php app/console doctrine:schema:update --force That's it! Your form now validates, and allows you to save the ``User`` -object to the database. The extra ``terms`` checkbox on the ``Registration`` -model class is used during validation, but not actually used afterwards when -saving the User to the database. +object to the database. The extra ``termsAccepted`` checkbox on the +``Registration`` model class is used during validation, but not actually used +afterwards when saving the User to the database. .. _`CVE-2013-5750`: http://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form