Skip to content

Commit

Permalink
[Cookbook] Make registration_form follow best practices
Browse files Browse the repository at this point in the history
  • Loading branch information
xelaris committed Jan 7, 2015
1 parent 1972757 commit 57d9cf2
Showing 1 changed file with 105 additions and 128 deletions.
233 changes: 105 additions & 128 deletions cookbook/doctrine/registration_form.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.. index::
single: Doctrine; Simple Registration Form
single: Form; Simple Registration Form
single: Security; Simple Registration Form

How to Implement a simple Registration Form
===========================================
Expand All @@ -10,13 +11,13 @@ 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 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;
Expand All @@ -36,7 +37,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()
*/
Expand All @@ -45,9 +46,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()
{
Expand All @@ -64,21 +65,27 @@ You have a simple ``User`` entity mapped to the database::
$this->email = $email;
}

public function getPlainPassword()
public function getPassword()
{
return $this->password;
}

public function setPassword($password)
{
return $this->plainPassword;
$this->password = $password;
}

public function setPlainPassword($password)
public function getSalt()
{
$this->plainPassword = $password;
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::

Expand All @@ -90,7 +97,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
Expand All @@ -101,13 +108,13 @@ 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 Entity
----------------------------

Next, create the form for the ``User`` model::
Next, create the form for the ``User`` entity::

// src/Acme/AccountBundle/Form/Type/UserType.php
namespace Acme\AccountBundle\Form\Type;
// src/AppBundle/Form/Type/UserType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
Expand All @@ -118,17 +125,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'
));
}

Expand All @@ -138,7 +145,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).

Expand All @@ -156,17 +163,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;
Expand Down Expand Up @@ -200,8 +207,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;
Expand All @@ -211,11 +218,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');
}

Expand All @@ -233,121 +238,93 @@ 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'),
));
$form = $this->createForm(new RegistrationType(), new Registration());

return $this->render(
'AcmeAccountBundle:Account:register.html.twig',
array('form' => $form->createView())
);
}
}
$form->handleRequest($request);

And its template:
if ($form->isSubmitted() && $form->isValid()) {
$registration = $form->getData();
$user = $registration->getUser();

.. code-block:: html+jinja
$password = $this
->get('security.encoder_factory')
->getEncoder($user)
->encodePassword(
$user->getPassword(),
$user->getSalt()
);
$user->setPassword($password);

{# src/Acme/AccountBundle/Resources/views/Account/register.html.twig #}
{{ form(form) }}
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();

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;
// ...
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 <routing-include-external-resources>`.

.. configuration-block::
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:

.. code-block:: yaml
.. code-block:: yaml
# src/Acme/AccountBundle/Resources/config/routing.yml
account_register:
path: /register
defaults: { _controller: AcmeAccountBundle:Account:register }
# app/config/security.yml
security:
encoders:
AppBundle\Entity\User: bcrypt
account_create:
path: /register/create
defaults: { _controller: AcmeAccountBundle:Account:create }
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 <book-security-encoding-user-password>`

.. code-block:: xml
<!-- src/Acme/AccountBundle/Resources/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">
.. note::

<route id="account_register" path="/register">
<default key="_controller">AcmeAccountBundle:Account:register</default>
</route>
While ``$form->isSubmitted()`` isn't technically needed, since isValid()
first calls `isSubmitted()`, it is recommended to use it to improve
readability.

<route id="account_create" path="/register/create">
<default key="_controller">AcmeAccountBundle:Account:create</default>
</route>
</routes>
And its template:

.. code-block:: php
.. code-block:: html+jinja

// src/Acme/AccountBundle/Resources/config/routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
{# app/Resources/views/account/register.html.twig #}
{{ form(form) }}

$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',
)));
Add new Routes
--------------

return $collection;
Don't forget to make sure that the routes defined as annotations in your
controller are :ref:`loaded <routing-include-external-resources>` by your main
routing configuration file.

Update your Database Schema
---------------------------
Expand All @@ -360,8 +337,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

0 comments on commit 57d9cf2

Please sign in to comment.