From 1c1b92a9706e659b768b7ef12f986eb4feabc485 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Tue, 28 Apr 2015 07:40:24 -0700 Subject: [PATCH 1/3] [WIP] Reworking most of the registration form: 1) Showing the embedded Registration model is way too hard and unnecessary 2) Stopped persisted plainPassword (why did we do this?) 3) Used AppBundle approach 4) Annotation routing various other small things --- cookbook/doctrine/registration_form.rst | 347 ++++++++++-------------- 1 file changed, 146 insertions(+), 201 deletions(-) diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst index 418c4f1a4e6..ac6f7899943 100644 --- a/cookbook/doctrine/registration_form.rst +++ b/cookbook/doctrine/registration_form.rst @@ -5,15 +5,21 @@ How to Implement a simple Registration Form =========================================== -Some forms have extra fields whose values don't need to be stored in the -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. +Creating a registration form is pretty easy - it *really* means just creating +a form that will update some ``User`` object (a Doctrine entity in this example) +and then save it. -The simple User Model ---------------------- +If you don't already have a ``User`` entity and a working login system, +first start with :doc:`/cookbook/security/entity_provider`. -You have a simple ``User`` entity mapped to the database:: +Your ``User`` entity will probably at least have the following fields: +* ``email`` +* ``username`` +* ``password`` (the encoded password) +* ``plainPassword`` (*not* persisted: notice no ``@ORM\Column`` above it) +* anything else you want + +With some validation added, it may look something like this:: // src/AppBundle/Entity/User.php namespace AppBundle\Entity; @@ -21,12 +27,14 @@ You have a simple ``User`` entity mapped to the database:: 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 + class User implements UserInterface { /** * @ORM\Id @@ -44,11 +52,26 @@ You have a simple ``User`` entity mapped to the database:: /** * @ORM\Column(type="string", length=255) + * @Assert\NotBlank() + */ + protected $username; + + /** * @Assert\NotBlank() * @Assert\Length(max = 4096) */ protected $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) + */ + protected $password; + + // other properties + public function getId() { return $this->id; @@ -64,6 +87,16 @@ You have a simple ``User`` entity mapped to the database:: $this->email = $email; } + public function getUsername() + { + return $this->username; + } + + public function setUsername($username) + { + $this->username = $username; + } + public function getPlainPassword() { return $this->plainPassword; @@ -73,18 +106,16 @@ You have a simple ``User`` entity mapped to the database:: { $this->plainPassword = $password; } - } -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. + public function setPassword($password) + { + $this->password = $password; + } + + // other methods, including security methods like getRoles() + } -.. note:: - If you want to integrate this User within the security system, you need - to implement the :ref:`UserInterface ` of the - Security component. .. _cookbook-registration-password-max: @@ -104,10 +135,10 @@ the class. Create a Form for the Model --------------------------- -Next, create the form for the ``User`` model:: +Next, create the form for the ``User`` entity:: - // src/AppBundle/Form/Type/UserType.php - namespace AppBundle\Form\Type; + // src/AppBundle/Form/UserType.php + namespace AppBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; @@ -117,12 +148,13 @@ 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('email', 'email'); + ->add('username', 'text'); + ->add('plainPassword', 'repeated', array( + 'type' => 'password', + ) + ); } public function setDefaultOptions(OptionsResolverInterface $resolver) @@ -138,98 +170,13 @@ Next, create the form for the ``User`` model:: } } -There are just two fields: ``email`` and ``plainPassword`` (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). +There are just three fields: ``email``, ``username`` and ``plainPassword`` +(repeated to confirm the entered password). .. tip:: To explore more things about the Form component, read :doc:`/book/forms`. -Embedding the User Form into a Registration Form ------------------------------------------------- - -The form that you'll use for the registration page is not the same as the -form used to simply modify the ``User`` (i.e. ``UserType``). The registration -form will contain further fields like "accept the terms", whose value won't -be stored in the database. - -Start by creating a simple class which represents the "registration":: - - // src/AppBundle/Form/Model/Registration.php - namespace AppBundle\Form\Model; - - use Symfony\Component\Validator\Constraints as Assert; - - use AppBundle\Entity\User; - - class Registration - { - /** - * @Assert\Type(type="AppBundle\Entity\User") - * @Assert\Valid() - */ - protected $user; - - /** - * @Assert\NotBlank() - * @Assert\True() - */ - protected $termsAccepted; - - public function setUser(User $user) - { - $this->user = $user; - } - - public function getUser() - { - return $this->user; - } - - public function getTermsAccepted() - { - return $this->termsAccepted; - } - - public function setTermsAccepted($termsAccepted) - { - $this->termsAccepted = (bool) $termsAccepted; - } - } - -Next, create the form for this ``Registration`` model:: - - // src/AppBundle/Form/Type/RegistrationType.php - namespace AppBundle\Form\Type; - - use Symfony\Component\Form\AbstractType; - use Symfony\Component\Form\FormBuilderInterface; - - class RegistrationType extends AbstractType - { - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('user', new UserType()); - $builder->add( - 'terms', - 'checkbox', - array('property_path' => 'termsAccepted') - ); - $builder->add('Register', 'submit'); - } - - public function getName() - { - return 'registration'; - } - } - -You don't need to use a special method for embedding the ``UserType`` form. -A form is a field, too - so you can add this like any other field, with the -expectation that the ``Registration.user`` property will hold an instance -of the ``User`` class. - Handling the Form Submission ---------------------------- @@ -241,127 +188,125 @@ controller for displaying the registration form:: use Symfony\Bundle\FrameworkBundle\Controller\Controller; - use AppBundle\Form\Type\RegistrationType; - use AppBundle\Form\Model\Registration; + use AppBundle\Form\UserType; + use AppBundle\Entity\User; + use Symfony\Component\HttpFoundation\Request; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; - class AccountController extends Controller + class RegistrationController extends Controller { - public function registerAction() + /** + * @Route("/register", name="user_registration") + */ + public function registerAction(Request $request) { - $registration = new Registration(); - $form = $this->createForm(new RegistrationType(), $registration, array( - 'action' => $this->generateUrl('account_create'), - )); + // 1) build the form + $user = new User(); + $form = $this->createForm(new UserType(), $user); + + // 2) handle the submit (will only happen on POST) + $form->handleRequest($request); + if ($form->isValid()) { + // save the User! + $em = $this->getDoctrine()->getManager(); + $em->persist($user); + $em->flush(); + + // do any other work - like send them an email, etc + // maybe set a "flash" success message for the user + + $redirectUrl = $this->generateUrl('replace_with_some_route'); + + return $this->redirect($redirectUrl); + } return $this->render( - 'AppBundle:Account:register.html.twig', + 'registration/register.html.twig', array('form' => $form->createView()) ); } } -And its template: - -.. code-block:: html+jinja - - {# src/AppBundle/Resources/views/Account/register.html.twig #} - {{ form(form) }} - -Next, create the controller which handles the form submission. This performs -the validation and saves the data into the database:: - - use Symfony\Component\HttpFoundation\Request; - // ... - - 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(...); - } +.. note:: - return $this->render( - 'AppBundle:Account:register.html.twig', - array('form' => $form->createView()) - ); - } + If you decide to NOT use annotation routing (shown above), then you'll + need to create a route to this controller: + + .. configuration-block:: -Add new Routes --------------- + .. code-block:: yaml -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 `. + # app/config/routing.yml + user_registration: + path: /register + defaults: { _controller: AppBundle:Registration:register } -.. configuration-block:: + .. code-block:: xml - .. code-block:: yaml + + + - # src/AppBundle/Resources/config/routing.yml - account_register: - path: /register - defaults: { _controller: AppBundle:Account:register } + + AppBundle:Registration:register + + - account_create: - path: /register/create - defaults: { _controller: AppBundle:Account:create } + .. code-block:: php - .. code-block:: xml + // 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', + ))); - - AppBundle:Account:register - + return $collection; - - AppBundle:Account:create - - +Next, create the template: - .. code-block:: php +.. code-block:: html+jinja - // src/AppBundle/Resources/config/routing.php - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; + {# app/Resources/views/registration/register.html.twig #} + + {{ form_start(form) }} + {{ form_row('form.username') }} + {{ form_row('form.email') }} - $collection = new RouteCollection(); - $collection->add('account_register', new Route('/register', array( - '_controller' => 'AppBundle:Account:register', - ))); - $collection->add('account_create', new Route('/register/create', array( - '_controller' => 'AppBundle:Account:create', - ))); + {{ form_row('form.plainPassword.first', { + 'label': 'Password' + }) }} + {{ form_row('form.plainPassword.second', { + 'label': 'Repeat Password' + }) }} - return $collection; + + {{ form_end(form) }} Update your Database Schema --------------------------- -Of course, since you've added a ``User`` entity during this tutorial, make -sure that your database schema has been updated properly: +If you've updated the ``User`` entity during this tutorial, make sure that +your database schema has been updated properly: .. code-block:: bash $ 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. +That's it! Head to ``/register`` to try things out! + +Having a Registration form with only Email (no Username) +-------------------------------------------------------- + +Todo + +Adding a "accept terms" Checkbox +-------------------------------- + +Todo .. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form From 8b7fc790e49713ee7b8d6c0b5d6e2000bb6ce636 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 14 Oct 2015 22:09:29 -0400 Subject: [PATCH 2/3] Tweaks thanks to everyone! --- cookbook/doctrine/registration_form.rst | 167 ++++++++++++++++++------ reference/forms/types/form.rst | 2 + 2 files changed, 129 insertions(+), 40 deletions(-) diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst index ac6f7899943..03cc6d85e8f 100644 --- a/cookbook/doctrine/registration_form.rst +++ b/cookbook/doctrine/registration_form.rst @@ -6,20 +6,37 @@ How to Implement a simple Registration Form =========================================== Creating a registration form is pretty easy - it *really* means just creating -a form that will update some ``User`` object (a Doctrine entity in this example) +a form that will update some ``User`` model object (a Doctrine entity in this example) and then save it. +.. tip:: + + The popular `FOSUserBundle`_ provides a registration form, reset password form + and other user management functionality. + If you don't already have a ``User`` entity and a working login system, first start with :doc:`/cookbook/security/entity_provider`. Your ``User`` entity will probably at least have the following fields: -* ``email`` -* ``username`` -* ``password`` (the encoded password) -* ``plainPassword`` (*not* persisted: notice no ``@ORM\Column`` above it) -* anything else you want -With some validation added, it may look something like this:: +``username`` + This will be used for logging in, unless you instead want your user to + :ref:`login via email ` (in that case, this + field is unnecessary). + +``email`` + A nice piece of information to collect. You can also allow users to + :ref:`login via email `. + +* ``password`` + The encoded password. + +* ``plainPassword`` + This field is *not* persisted: (notice no ``@ORM\Column`` above it). It + temporarily stores the plain password from the registration form. This field + can be validated then used to populate the ``password`` field. + +With some validation added, your class may look something like this:: // src/AppBundle/Entity/User.php namespace AppBundle\Entity; @@ -41,26 +58,26 @@ With some validation added, it may look something like this:: * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ - protected $id; + private $id; /** * @ORM\Column(type="string", length=255) * @Assert\NotBlank() * @Assert\Email() */ - protected $email; + private $email; /** * @ORM\Column(type="string", length=255) * @Assert\NotBlank() */ - protected $username; + private $username; /** * @Assert\NotBlank() * @Assert\Length(max = 4096) */ - protected $plainPassword; + private $plainPassword; /** * The below length depends on the "algorithm" you use for encoding @@ -68,14 +85,9 @@ With some validation added, it may look something like this:: * * @ORM\Column(type="string", length=64) */ - protected $password; + private $password; - // other properties - - public function getId() - { - return $this->id; - } + // other properties and methods public function getEmail() { @@ -115,7 +127,9 @@ With some validation added, it may look something like this:: // other methods, including security methods like getRoles() } - +The ``UserInterface`` requires a few other methods and your ``security.yml`` file +needs to be configured properly to work with the ``User`` entity. For a more full +example, see the :ref:`Entity Provider ` article. .. _cookbook-registration-password-max: @@ -152,7 +166,9 @@ Next, create the form for the ``User`` entity:: ->add('email', 'email'); ->add('username', 'text'); ->add('plainPassword', 'repeated', array( - 'type' => 'password', + 'type' => 'password', + 'first_options' => array('label' => 'Password'), + 'second_options' => array('label' => 'Repeat Password'), ) ); } @@ -206,13 +222,19 @@ controller for displaying the registration form:: // 2) handle the submit (will only happen on POST) $form->handleRequest($request); - if ($form->isValid()) { - // save the User! + if ($form->isValid() && $form->isSubmitted()) { + // 3) Encode the password (you could also do this via Doctrine listener) + $encoder = $this->get('security.encoder_factory') + ->getEncoder($user); + $password = $encoder->encodePassword($user->getPlainPassword(), $user->getSalt()); + $user->setPassword($password); + + // 4) save the User! $em = $this->getDoctrine()->getManager(); $em->persist($user); $em->flush(); - // do any other work - like send them an email, etc + // ... do any other work - like send them an email, etc // maybe set a "flash" success message for the user $redirectUrl = $this->generateUrl('replace_with_some_route'); @@ -269,29 +291,42 @@ controller for displaying the registration form:: Next, create the template: -.. code-block:: html+jinja +.. configuration-block:: - {# app/Resources/views/registration/register.html.twig #} + .. code-block:: html+jinja + + {# 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') }} + + + {{ form_end(form) }} - {{ form_start(form) }} - {{ form_row('form.username') }} - {{ form_row('form.email') }} + .. code-block:: html+php + + - {{ form_row('form.plainPassword.first', { - 'label': 'Password' - }) }} - {{ form_row('form.plainPassword.second', { - 'label': 'Repeat Password' - }) }} + start($form) ?> + row($form['username']) ?> + row($form['email']) ?> - - {{ form_end(form) }} + row($form['plainPassword']['first']) ?> + row($form['plainPassword']['second']) ?> + + + end($form) ?> + +See :doc:`/cookbook/form/form_customization` for more details. Update your Database Schema --------------------------- -If you've updated the ``User`` entity during this tutorial, make sure that -your database schema has been updated properly: +If you've updated the User entity during this tutorial, you have to update your +database schema using this command: .. code-block:: bash @@ -299,14 +334,66 @@ your database schema has been updated properly: That's it! Head to ``/register`` to try things out! +.. _registration-form-via-email: + Having a Registration form with only Email (no Username) -------------------------------------------------------- -Todo +If you want your users to login via email and you don't need a username, then you +can remove it from your ``User`` entity entirely. Instead, make ``getUsername()`` +return the ``email`` property. + + // src/AppBundle/Entity/User.php + // ... + + class User implements UserInterface + { + // ... + + public function getUsername() + { + return $this->email; + } + + // ... + } + +Next, just update the ``providers`` section of your ``security.yml`` so that Symfony +knows to load your users via the ``email`` property on login. See +:ref:`authenticating-someone-with-a-custom-entity-provider`. Adding a "accept terms" Checkbox -------------------------------- -Todo +Sometimes, you want a "Do you accept the terms and conditions" checkbox on your +registration form. The only trick is that you want to add this field to your form +without adding an unnecessary new ``termsAccepted`` property to your ``User`` entity +that you'll never need. + +To do this, add a ``termsAccepted`` field to your form, but set its :ref:`mapped ` +option to ``false``:: + + // src/AppBundle/Form/UserType.php + // ... + use Symfony\\Component\\Validator\\Constraints\\IsTrue; + + class UserType extends AbstractType + { + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('email', 'email'); + // ... + ->add('termsAccepted', 'checkbox', array( + 'mapped' => false, + 'constraints' => new IsTrue(), + )) + ); + } + } + +The :ref:`constraints ` option is also used, which allows +us to add validation, even though there is no ``termsAccepted`` property on ``User``. .. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form +.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle diff --git a/reference/forms/types/form.rst b/reference/forms/types/form.rst index ac3b96d5a0c..1478fb0c8e0 100644 --- a/reference/forms/types/form.rst +++ b/reference/forms/types/form.rst @@ -95,6 +95,8 @@ The actual default value of this option depends on other field options: .. include:: /reference/forms/types/options/label_attr.rst.inc +.. _reference-form-option-mapped: + .. include:: /reference/forms/types/options/mapped.rst.inc .. _reference-form-option-max_length: From a8275a38bbfd0c6eddd52015c6cef1afeed1f5b1 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 14 Oct 2015 22:15:39 -0400 Subject: [PATCH 3/3] Fixing build error --- cookbook/doctrine/registration_form.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/doctrine/registration_form.rst b/cookbook/doctrine/registration_form.rst index 03cc6d85e8f..956026248c1 100644 --- a/cookbook/doctrine/registration_form.rst +++ b/cookbook/doctrine/registration_form.rst @@ -341,7 +341,7 @@ Having a Registration form with only Email (no Username) If you want your users to login via email and you don't need a username, then you can remove it from your ``User`` entity entirely. Instead, make ``getUsername()`` -return the ``email`` property. +return the ``email`` property:: // src/AppBundle/Entity/User.php // ...