Registration Form - Part 2


In this video we are continuing on with our Symfony registration form implementation. Towards the end of the previous video we had created our RegistrationType - the form 'blueprint', along with a very simple twig template to display the form, and a corresponding RegistrationController, to tie everything together.

The reassuring part at this point is that we've already covered how to handle a form submission in Symfony. Whilst Registration is a little more complicated in overall scope, the general process of submitting a form - be it simple or complex - is exactly the same. Learn it once, repeat it everywhere.

Let's add in form submission handling now:

<?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;
use Symfony\Component\HttpFoundation\Request;

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

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

        $form->handleRequest($request);

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

Ok, so what's new here?

Well, we have added in the immediately obvious:

$form->handleRequest($request);

If we want to work with a form submission then as covered in previous videos we need this line.

Less obvious is that we have had to inject the $request object in to our registerAction:

registerAction(Request $request)

And add in the corresponding use statement:

use Symfony\Component\HttpFoundation\Request;

Next, again we know by now that after calling handleRequest we can then determine if the form has been submitted, and if the submitted data is valid. Previously we didn't use any validation logic, but for registration this will become more important. After all, we don't want two users with the same username, for example.

Let's prepare ourselves for processing the form submission:

<?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;
use Symfony\Component\HttpFoundation\Request;

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

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

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            // new logic here
        }

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

Ok so there's some extra work that we need to do here.

We're going to need to encode the user's password. As already mentioned in the previous video, we are going to receive the user's password in plain text. We need to run this password through the bcrypt encoder. And then we need to ensure we only save off that resulting value, not the plain text submission.

Following on from this, we will need to save (or in Doctrine lingo, persist and flush) the resulting Member entity off to the database.

And then there's one sneaky gotcha that maybe isn't immediately obvious. Just because we successfully register, doesn't mean that our user / Member will be immediately logged in. Yep, this is a manual process that we as developers must implement. It's these sort of fun little details that so often get missed, and can take up a not-insignificant amount of time if it's your first time doing something like this.

Let's not dwell on this last point for the moment. Instead, let's start with the most obvious point - encoding the user's password.

Encoding A User's Password

Encoding a password sounds quite complex. Complex, in my case, usually leads to a little bit of fear. And fear is a real killer of forward progress.

Fortunately, as you have hopefully come to expect by now, this isn't going to be quite as hard as it may initially seem. One of the big benefits of using a modern framework such as Symfony is that these problems are already fixed for us. There's already a solution, we just need to use it.

If you remember back to when we implemented login, we hit upon the problem that we couldn't yet register our users via our front end, as we hadn't yet configured that part of our site.

So, instead, we resorted to 'hard coding' our users via the in_memory provider.

Along the way we saw that we must configure an encoder for our users, for which we used the bcrypt algorithm:

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt

    providers:
        in_memory:
            memory:
                users:
                    admin:
                        password:
                        roles: 'ROLE_ADMIN'

Now, when using the in_memory user provider we implicitly created a user of type Symfony\Component\Security\Core\User\User.

This matches up to the entry we had to add in to the encoders section, and if you recall, we were told about this through a runtime exception when trying to manually encode our password.

This time, however, we are working with our Member entity. And so that means we need to add in another entry under encoders that matches up to the class path for our Member entity:

# /app/config/security.yml

security:

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
        AppBundle\Entity\Member:
            algorithm: bcrypt

    providers:
        in_memory:
            # snip

Easy enough.

Now that Symfony knows how to encode (or, technically, hash) passwords for Member entities we can go ahead and add code in to do just that.

<?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;
use Symfony\Component\HttpFoundation\Request;

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

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

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {

            $password = $this
                ->get('security.password_encoder')
                ->encodePassword(
                    $member,
                    $member->getPlainPassword()
                )
            ;

            $member->setPassword($password);

            $em = $this->getDoctrine()->getManager();

            $em->persist($member);
            $em->flush();

        }

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

As of Symfony 2.6 onwards, the security.password_encoder service has been made available to us as part of the Symfony framework. We just have to make sure that we specify our encoder - which we just did in security.yml - and then we can start using this service in our controllers (or inject it into services - bit more advanced, but not that much more) without any further effort. Very nice.

One thing to point out with this service is that there need be no mystery to what it is, or how to find out more about it:

php bin/console debug:container security.password_encoder

Information for Service "security.password_encoder"
===================================================

 ------------------ ------------------------------------------------------------- 
  Option             Value                                                        
 ------------------ ------------------------------------------------------------- 
  Service ID         security.password_encoder                                    
  Class              Symfony\Component\Security\Core\Encoder\UserPasswordEncoder  
  Tags               -                                                            
  Public             yes                                                          
  Synthetic          no                                                           
  Lazy               no                                                           
  Shared             yes                                                          
  Abstract           no                                                           
  Autowired          no                                                           
  Autowiring Types   -                                                            
 ------------------ ------------------------------------------------------------- 

Feel free to open up the class Symfony\Component\Security\Core\Encoder\UserPasswordEncoder and inside you will see the encodePassword method:

    /**
     * {@inheritdoc}
     */
    public function encodePassword(UserInterface $user, $plainPassword)
    {
        $encoder = $this->encoderFactory->getEncoder($user);

        return $encoder->encodePassword($plainPassword, $user->getSalt());
    }

From the encodePassword method signature we can see that we need to pass in something implementing UserInterface, and our plain password.

Fortunately we have both of these things. Our Member entity implements UserInterface. And our form submission contains the plain password! Bonza.

Knowing all this, we can create an encoded password:

$password = $this
    ->get('security.password_encoder')
    ->encodePassword(
        $member,
        $member->getPlainPassword()
    )
;

Remember, our $member entity / object has been populated for us as part of the form submission process. It's nice how all this is coming together, isn't it?

With our encoded $password created, we now just need to set this value on to our $member entity, and then use the Doctrine entity manager to save this change off to the database:

$member->setPassword($password);

$em = $this->getDoctrine()->getManager();

$em->persist($member);
$em->flush();

Now, there is a little more to working with the database than this, as you might expect, but we won't dive too deep into this at this point. If you are keen to get to grips with Doctrine straight away then I recommend you take the Doctrine Databasics course as your next step.

There are still issues to address here. We aren't logged in after registering, and we have no validations in place to stop duplicate usernames and email addresses being submitted. This is something we will get on with fixing in the very next video.

PHPStorm Plugin

Throughout this video, and others on this site, I make use of one of my favourite PHPStorm plugins - PHP Inspections EA Extended. If you use PHPStorm, I would strongly recommend this plugin to you. It's awesome.

Code For This Course

Get the code for this course.

Episodes