Registration Form - Part 1


In this video we are going to make a start on our Registration Form. A few videos back we created a Member entity - a plain old PHP class that has been augmented with a few annotations to enable it to be used by Doctrine to represent the concept of a User (or ahem, Member) in our system.

Let's quickly recap the contents of our Member entity:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member implements UserInterface, \Serializable
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="username", type="string", length=255, unique=true)
     */
    private $username;

    /**
     * @var string
     *
     * @ORM\Column(name="email", type="string", length=255, unique=true)
     */
    private $email;

    /**
     * @var string
     *
     * @ORM\Column(name="password", type="string", length=64)
     */
    private $password;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    // other things remove for brevity
}

The important points at this stage are the this Member entity implements UserInterface and \Serializable, and that we have the appropriate annotations configured to save data from this entity off to our database.

If you are at all unsure on this, please consider watching this video first.

Now, we used the Doctrine entity generator to create this entity, and we specified that we wanted a password property along the way.

The thing is, the value we store in this field will be the encoded / bcrypt encrypted password. The idea here is that we will never store the user's plain text password. Instead, all future requests to log in will involve running the bcrypt process against the user's submitted plain text password and comparing that value to the value we have stored on our entity. If the two match, then that means they sent in the right password.

However, whilst we won't persist the user's plain text password, we do need a temporary holding area to store the plain text password that is submitted from the Registration Form.

To begin with, let's add a new class property to hold this plain text password information. And as we won't be persisting this info, we don't need any annotations. Though we will need a getter and a setter:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member implements UserInterface, \Serializable
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="username", type="string", length=255, unique=true)
     */
    private $username;

    /**
     * @var string
     *
     * @ORM\Column(name="email", type="string", length=255, unique=true)
     */
    private $email;

    private $plainPassword;

    /**
     * @var string
     *
     * @ORM\Column(name="password", type="string", length=64)
     */
    private $password;

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    /**
     * @param string $plainPassword
     * @return Member
     */
    public function setPlainPassword($plainPassword)
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }

    // other things remove for brevity
}

Continuining on with the theme of using Symfony's build in generators to save ourselves a little typing, we can now go ahead and generate a form that corresponds to this entity:

php bin/console doctrine:generate:form AppBundle:Member

created ./src/AppBundle/Form/MemberType.php
The new MemberType.php class file has been created under /path/to/your/project/src/AppBundle/Form/MemberType.php.

I prefer to have my form types stored under /src/AppBundle/Form/Type, so I am going to immediately move the generated form and update the namespace accordingly:

<?php

// /src/AppBundle/Form/Type/MemberType.php

namespace AppBundle\Form\Type; // <-- added \Type here

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MemberType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('username')->add('email')->add('password');
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Member'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_member';
    }
}

This is a good start, but there's still work to be done.

Firstly, I'm going to tidy up the configure options section:

use AppBundle\Entity\Member;

    // * snip *

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Member::class
        ]);
    }

I prefer the short hand array syntax, and using the class constants (::class) is a practice now widely used in Symfony 3.

Next, as covered, we are not going to directly work with the password property. We will first use the plainPassword property to store the plain password, and then save the encoded password to the password property:

use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;

    // * snip *

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('username', TextType::class)
            ->add('email', EmailType::class)
            ->add('plainPassword', RepeatedType::class, [
                'type' => PasswordType::class,
                'first_options' => [
                    'label' => 'Password',
                ],
                'second_options' => [
                    'label' => 'Repeat Password'
                ]
            ])
            ->add('register', SubmitType::class)
        ;
    }

Ok so there's quite a lot of change here.

As covered already in this series, the form field types simply help render appropriate form fields with HTML5 validations in our front end. If we use a EmailType for example, we get an input with a type=email. This makes our generated HTML that much nicer, but isn't any real form of validation. It's just nice to have as it is more helpful to the user.

That covers the easy two - username and email.

Then, password has been replaced with plainPassword.

We've already covered why this is.

But what we now want to do is ensure the user doesn't "fluff their lines". A common practice is to make the user enter their password, then repeat it again to double check they didn't make a typo. If they make two typos then really there's no helping them :) Well, there is, but unfortunately it involves a support ticket.

Anyway, the RepeatedType is a special form field type that renders two inputs of a given type.

Most commonly this would be used with the PasswordType. And indeed, that's what we are going to do here.

first_options and second_options are fairly self explanatory. We have two fields and these options related to the first and second rendered inputs.

There are a few more options to play with here, so be sure to read the documentation.

Lastly we add a button to allow the end user to submit the form.

Remember to add the appropriate use statements for each used form field type.

Rendering Our Registration Form

At this point we have our form type - which you might think of as a form of 'blueprint' that represents our form. This form type alone is not enough to display the form. For that, we need a controller and associated template.

The template we need is super straightforward:

<!-- /app/Resources/views/registration/register.html.twig -->

{% extends 'base.html.twig' %}

{% block body %}
    {{ form(registration_form) }}
{% endblock %}

We haven't yet created our RegistrationController, but when we do, we can determine from this template - specifically from the form twig function - that we will need to pass in a form view under the key of registration_form.

Let's go ahead and create that controller:

<?php

// /src/AppBundle/Controller/RegistrationController.php

namespace AppBundle\Controller;

use AppBundle\Entity\Member;
use AppBundle\Form\Type\MemberType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class RegistrationController extends Controller
{
    /**
     * @Route("/register", name="registration")
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function registerAction()
    {
        $member = new Member();

        $form = $this->createForm(MemberType::class, $member);

        return $this->render('registration/register.html.twig', [
            'registration_form' => $form->createView(),
        ]);
    }
}

Ok, let's break this down.

The good news is, even though there's a fair amount of 'stuff' here, there's very little we haven't already seen in this course.

We have our namespace, which is almost a direct mapping between where our controller lives in our project's directory structure. Typically all controllers live in AppBundle\Controller.

We have all our use statements. Using an IDE like PHPStorm takes a lot of the hassle of remembering to add these - or find the right paths - out of your hands.

Our RegistrationController extends Symfony's Controller from the FrameworkBundle.

We have our @Route annotation which dictates the path we need to hit to see the contents of this action be displayed. We have name'd the route so that we can refer to it by name when generating routes in our twig templates.

registerAction is the name of our controller action, and follows the Symfony convention of naming our route with a lowercase word and then Action as the suffix.

Skipping ahead a few lines we have the return statement which returns the rendered output of our register twig template, into which we are passing the outcome of calling createView on our registration form. We pass this value through under the registration_form key, which we know we need in our template for the form twig function to properly process.

Phew, that's quite a lot of stuff.

The new bit is in new'ing up a Member entity. Up until now we have used null as the data for our forms.

At this stage we aren't handling the submitted request - that's coming in the next video - but for the moment we are simply passing in an empty Member entity as the data class for our MemberType form. As we shall soon see, this entity will be helpfully populated with all the data that the user submits when they fill in the registration form.

You should now be able to browse to /register in your application and see the Registration Form rendered out to screen. Whilst filling in this form and clicking submit won't quite have the expected outcome just yet, we are well on our way to a working registration form, and it's really not taken us too long to get here.

Code For This Course

Get the code for this course.

Episodes